【sgAutocomplete_v2】自定义组件:基于elementUI的el-input组件开发的搜索输入框(支持本地保存历史搜索关键词、后台获取匹配项)

发布于:2025-03-21 ⋅ 阅读:(17) ⋅ 点赞:(0)

特性:

  1. 支持本地记录搜索关键词
  2. 后台接口匹配搜索关键词
  3. 支持自定义填充字段名
  4. 支持user或address两种匹配列表布局样式

sgAutocomplete_v2 

<template>
  <div :class="$options.name" @mouseover="inside = true" @mouseout="inside = false">
    <div class="search-layer">
      <el-input
        class="search-input"
        v-model.trim="inputSearchValue"
        maxlength="50"
        :show-word-limit="false"
        :placeholder="`你在找什么?快告诉我...`"
        clearable
        @keyup.native.enter="search"
        @focus="focusSearchInput"
        @clear="clearSearchInput"
        @input="inputingSearchInput"
      >
        <el-button slot="append" icon="el-icon-search" @click="search"></el-button>
      </el-input>
    </div>

    <div class="absolute-dropdownlist-layer" v-if="showAbsoluteDropdownlistLayer">
      <!-- 搜索建议 -->
      <div
        class="search-suggestion"
        v-if="historyKeywords.length > 0 && visible_search_suggestion"
      >
        <!-- 搜索历史 -->
        <div class="search-suggestion-item history">
          <div class="search-suggestion-item-head">
            <h1>最近搜索</h1>
            <el-link
              icon="el-icon-delete"
              type="danger"
              :underline="false"
              @click.stop="clearAll"
              >清除
            </el-link>
          </div>
          <ul class="historyKeywords">
            <li v-for="(a, i) in historyKeywords" :key="i">
              <el-tag
                :title="a"
                type="info"
                closable
                @click.stop="clickKeyword(a)"
                @close="closeTag(a)"
              >
                <span class="text">{{ a }}</span></el-tag
              >
            </li>
          </ul>
        </div>
      </div>
      <!-- 后台查询推荐匹配项 -->
      <div class="matchList" v-if="matchList.length > 0">
        <ul>
          <li
            v-for="(a, i) in matchList"
            :key="i"
            @click.stop="clickMatchItem(a)"
            :type="matchType"
          >
            <template v-if="matchType == `user`">
              <div class="face">
                <img
                  :src="a.src"
                  v-if="a.src && !a.hideImg"
                  @error="error($event, a, i)"
                />
                <el-avatar
                  v-else
                  :style="
                    $g.convertData({ obj: `ROLE.PROFESSIONAL`, value: a.type }).style
                  "
                >
                  {{
                    a.username
                      ? a.username.slice(-1)
                      : a[matchKeyID] === "master"
                      ? "超"
                      : "未"
                  }}
                </el-avatar>
              </div>
              <b class="username" v-html="a.username_HTML"> </b>
              <span class="userid" v-html="a.userid_HTML"></span>
              <span
                class="unit"
                v-html="`${a.unit_HTML ? `(${a.unit_HTML})` : ``}`"
              ></span>
            </template>
            <template v-if="matchType == `address`">
              <b class="username" v-html="a.username_HTML"> </b>
              <span
                class="address"
                v-html="`${a.address_HTML ? `[${a.address_HTML}]` : ``}`"
              ></span>
            </template>
          </li>
        </ul>
        <div class="foot" v-if="showMoreBtn">
          <div class="more-btn" @click.stop="readmoreMatch" :loading="moreBtnLoading">
            {{ moreBtnLoading ? `加载中…` : `查看更多` }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "sgAutocomplete_v2",
  components: {},
  data() {
    return {
      form: {},
      // ----------------------------------------
      inside: false, //鼠标是否在组件内
      showAbsoluteDropdownlistLayer: false, //显示absolute-dropdownlist-layer
      inputSearchValue: this.$route.query.keyword || "", //搜索关键词
      // 搜索建议----------------------------------------
      visible_search_suggestion: false, //显示搜索建议
      historyKeywords: [],
      // 搜索匹配----------------------------------------
      visible_search_match: false, //显示搜索匹配
      mainID: `ID`, //主键
      matchKeyID: `NAME`, //当点击匹配项,自动填充输入框的字段
      matchType: `user`, //默认匹配类型是user,枚举值:user、address
      matchList: [],
      keywordClassName: "match-autocomplete-keyword", //匹配高亮关键词样式
      showMoreBtn: false,
      moreBtnLoading: false,
      // ----------------------------------------
    };
  },
  props: ["value", "data"],
  computed: {},
  watch: {
    value: {
      handler(d) {
        this.inputSearchValue = d;
      },
      deep: true,
      immediate: true,
    },
    inputSearchValue(d) {
      this.$emit("input", d);
    },

    data: {
      handler(newValue, oldValue) {
        //console.log(`深度监听${this.$options.name}:`, newValue, oldValue);
        if (Object.keys(newValue || {}).length) {
          this.form = JSON.parse(JSON.stringify(newValue));
          this.form.matchType && (this.matchType = this.form.matchType);
          this.form.matchKeyID && (this.matchKeyID = this.form.matchKeyID);
          this.showMoreBtn = this.form.showMoreBtn;
          this.moreBtnLoading = this.form.moreBtnLoading;
          this.matchList = this.form.matchList || [];
          this.matchList = [
            ...new Set(this.matchList.map((v) => v[this.matchKeyID])),
          ].map((ID) => this.matchList.find((v) => v[this.matchKeyID] === ID)); //去重同样的ID数据
          this.setHighlightMatchList();
          this.$nextTick(() => {
            let matchListScrollDOM = this.$el.querySelector(`.matchList>ul`);
            matchListScrollDOM &&
              matchListScrollDOM.scrollTo({
                top: matchListScrollDOM.scrollHeight,
                behavior: "smooth",
              });
          });
        }
      },
      deep: true, //深度监听
      immediate: true, //立即执行
    },
    matchList: {
      handler(newValue, oldValue) {
        //console.log(`深度监听${this.$options.name}:`, newValue, oldValue);
        if (Object.keys(newValue || {}).length) {
          newValue.length > 0 && (this.visible_search_suggestion = false);
        }
      },
      deep: true, //深度监听
      immediate: true, //立即执行
    },
  },
  created() {
    this.initHistoryKeywords();
  },
  mounted() {
    this.__add();
  },
  destroyed() {
    this.__remove();
  },
  methods: {
    // 搜索输入框----------------------------------------
    search({ keyword = this.inputSearchValue } = {}) {
      this.visible_search_suggestion = false;
      this.saveHistoryKeywords();
      this.$emit(`search`, { keyword });
    },
    inputingSearchInput(keyword) {
      if (keyword === "") {
        this.visible_search_suggestion = true;
      }
      this.$emit(`change`, { keyword, isReset: true });
    },
    clearSearchInput(d) {
      this.visible_search_suggestion = true;
    },
    getmatchList({ matchList } = {}) {
      if (matchList.length > 0) {
        this.visible_search_suggestion = false;
      }
    },
    focusSearchInput(d) {
      this.showAbsoluteDropdownlistLayer = true;
      if (this.inputSearchValue.length === 0) {
        this.visible_search_suggestion = this.matchList.length === 0;
      } else {
        this.visible_search_suggestion = false;
        this.$emit(`change`, { keyword: this.inputSearchValue, isReset: true });
      }
    },
    saveHistoryKeywords(d) {
      let historyKeywords = (localStorage.historyKeywords || "").split("|");
      historyKeywords.unshift(this.inputSearchValue);
      historyKeywords = historyKeywords.filter(Boolean); //清空数组中的empty、undefined、null
      historyKeywords = [...new Set(historyKeywords)];
      this.historyKeywords = historyKeywords;
      localStorage.historyKeywords = historyKeywords.join("|");
    },
    // 搜索建议----------------------------------------
    //初始化历史搜索记录
    initHistoryKeywords({ d } = {}) {
      this.historyKeywords = [
        ...new Set(
          (localStorage.historyKeywords || "").split("|").filter((v, i, ar) => v !== ``)
        ),
      ];
    },
    clickKeyword(d) {
      this.inputSearchValue = d;
      this.visible_search_suggestion = false;
      this.search();
      this.$nextTick(() => {
        this.focusSearchInput();
      });
    },
    closeTag(d) {
      this.historyKeywords = this.historyKeywords.filter((v, i, ar) => v !== d);
      localStorage.historyKeywords = this.historyKeywords.join("|");
    },
    clearAll(d) {
      this.visible_search_suggestion = false;
      this.historyKeywords = [];
      delete localStorage.historyKeywords;
    },
    // 搜索匹配----------------------------------------
    error($event, a, i) {
      this.$set(this.matchList[i], "hideImg", true);
    },
    clickMatchItem(a) {
      this.matchList = [];
      this.inputSearchValue = a[this.matchKeyID];
      this.search();
    },
    // 高亮匹配内容里面的关键词
    setHighlightMatchList() {
      let cls = this.keywordClassName;
      let keyword = this.inputSearchValue;
      this.matchList.forEach((d) => {
        Object.keys(d || {}).forEach((k) => {
          let originContent = this.$g.stripHTML(d[k]);
          d[`${k}_HTML`] = this.$g.highLightMatchString(originContent, keyword, cls);
        });
      });
    },
    readmoreMatch(d) {
      this.$emit(`readmoreMatch`, d);
    },
    // 全局监听----------------------------------------
    __add() {
      this.__remove();
      addEventListener("mousedown", this.__mousedown);
    },
    __remove() {
      removeEventListener("mousedown", this.__mousedown);
    },
    __mousedown(e) {
      this.inside || this.__hide(e); // 点击其他区域隐藏
    },
    __hide(e) {
      this.$emit("hide", e);
      this.showAbsoluteDropdownlistLayer = false;
    },
    __show(e) {
      this.$emit("show", e);
      this.showAbsoluteDropdownlistLayer = true;
    },
  },
};
</script>
<style lang="scss" scoped>
.sgAutocomplete_v2 {
  // 输入框搜索图层
  .search-layer {
    position: relative;
    z-index: 1;
    margin-bottom: 5px;
    .search-input {
    }
  }
  // 建议区域图层
  .absolute-dropdownlist-layer {
    z-index: 1;
    position: relative;
    width: 100%;
    $maxHeight: 400px; //下拉框最大高度
    // 搜索建议
    .search-suggestion {
      max-height: $maxHeight;
      position: absolute;
      top: 0;
      left: 0;

      box-shadow: 0 5px 20px 0 #00000022;
      background-color: white;
      border-radius: 4px;
      font-size: 14px;
      width: 100%;
      box-sizing: border-box;
      padding: 20px;
      .search-suggestion-item {
        margin-bottom: 20px;
        &:last-of-type {
          margin-bottom: 0;
        }
        .search-suggestion-item-head {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 20px;
          h1 {
            font-size: 14px;
            color: #2c343e;
          }
        }
        // 搜索历史

        .historyKeywords {
          display: flex;
          flex-wrap: wrap;
          margin-right: -10px;
          margin-bottom: -10px;
          max-height: calc(#{$maxHeight} - 80px);
          overflow-y: auto;

          li {
            margin-right: 10px;
            margin-bottom: 10px;
            >>> .el-tag {
              cursor: pointer;
              $tagMaxWidth: 200px;
              max-width: $tagMaxWidth;
              display: flex;
              align-items: center;
              &:hover,
              &:focus,
              &:active {
                border-color: #999;
                background-color: #00000009;
                span {
                  color: black;
                }
              }
              .text {
                max-width: calc(#{$tagMaxWidth} - 20px);
                overflow: hidden;
                white-space: nowrap;
                text-overflow: ellipsis;
                display: inline-block;
              }
              .el-tag__close {
                flex-shrink: 0;
              }
            }
          }
        }
      }
    }

    // 搜索匹配
    .matchList {
      max-height: $maxHeight;
      position: absolute;
      top: 0;
      left: 0;

      box-shadow: 0 5px 20px 0 #00000022;
      background-color: white;
      border-radius: 8px;
      font-size: 14px;
      width: 100%;
      box-sizing: border-box;
      padding: 10px;

      ul {
        width: 100%;
        max-height: calc(#{$maxHeight} - 80px);
        overflow-y: auto;
        li {
          width: 100%;
          display: flex;
          align-items: center;
          box-sizing: border-box;
          padding: 10px;
          cursor: pointer;
          transition: 0.2s;
          border-radius: 8px;
          &[type="user"] {
          }
          &[type="address"] {
          }
          &:hover {
            filter: brightness(1.1);
            background-color: #00000009;
          }
          .face {
            margin-right: 10px;
            img,
            .el-avatar {
              width: 30px;
              height: 30px;
              display: flex;
              justify-content: center;
              align-items: center;
              object-position: center;
              object-fit: cover;
              border-radius: 88px;
              overflow: hidden;
              background-color: #00000009;
            }
          }
          .username {
            margin-right: 5px;
            color: black;
          }
          .userid {
            color: #999;
            margin-right: 10px;
          }
          .unit {
            color: #999;
          }
          .address {
            color: #999;
          }
          // 匹配高亮关键词样式
          >>> .match-autocomplete-keyword {
            color: #409eff;
          }
        }
      }
      .foot {
        box-sizing: border-box;
        display: flex;
        justify-content: center;
        align-items: center;
        .more-btn {
          padding: 10px;
          width: 100%;
          height: 100%;
          text-align: center;
          border-radius: 4px;
          cursor: pointer;
          transition: 0.2s;
          color: #409eff;
          &[loading] {
            pointer-events: none;
          }
          &:hover {
            background-color: #00000009;
          }
        }
      }
    }
  }
}
</style>

demo

<template>
  <div :class="$options.name" style="display: flex; flex-wrap: nowrap">
    <sgAutocomplete_v2
      style="width: 500px"
      :data="data_sgAutocomplete_v2"
      v-model="inputValue"
      @change="changeMatch"
      @readmoreMatch="readmoreMatch"
      @search="search"
    />

    <el-radio-group
      v-model="data_sgAutocomplete_v2.matchType"
      size="small"
      style="margin-left: 10px; margin-top: 10px"
    >
      <el-radio
        v-for="(radio, index) in radios"
        :key="index"
        :label="radio.value"
        :disabled="radio.disabled"
        >{{ radio.label }}</el-radio
      >
    </el-radio-group>
  </div>
</template>
<script>
import sgAutocomplete_v2 from "@/vue/components/admin/sgAutocomplete_v2";

export default {
  name: "demoSgAutocomplete_v2",
  components: { sgAutocomplete_v2 },
  data() {
    return {
      radios: [
        { value: `user`, label: "用户" },
        { value: `address`, label: "地址" },
      ],
      // ----------------------------------------
      inputValue: ``,
      data_sgAutocomplete_v2: {
        matchType: `user`, //默认匹配类型是user,枚举值:user、address
        matchKeyID: `NAME`, //当点击匹配项,自动填充输入框的字段
        matchList: [], //匹配项列表
      },
      // ----------------------------------------

      currentPage: 1,
      pageSize: 6,
    };
  },

  methods: {
    // 动态获取匹配项
    changeMatch({ keyword = this.inputValue, isReset } = {}) {
      if (!keyword) return (this.data_sgAutocomplete_v2.matchList = []);
      if (isReset) {
        this.data_sgAutocomplete_v2.matchList = [];
        this.currentPage = 1;
      }
      let data = {
        start: this.currentPage - 1, //当前页数(从0开始)
        limit: this.pageSize, //每页显示条目个数
        KEY: keyword, //匹配关键词
      };
      this.$f.biz_person_query(
        {
          ...data,
          l: {
            show: () => this.$set(this.data_sgAutocomplete_v2, "moreBtnLoading", true),
            close: () => this.$set(this.data_sgAutocomplete_v2, "moreBtnLoading", false),
          },
          cb: (d) => {
            //回调函数
            if (isReset) {
              this.data_sgAutocomplete_v2.matchList = []; //避免重复追加
            }
            let old_matchList = this.data_sgAutocomplete_v2.matchList;
            let matchList = old_matchList.concat(d.data || []);
            //转义适配字段
            matchList.forEach((v) => {
              v.type = v.TYPE;
              v.src = v.PHOTO_T ? this.$d.responseFile(v.PHOTO_T) : null; //头像路径

              v.username = v.NAME;
              v.userid = v.ACCOUNT;
              v.unit = v.UNIT_ID_TEXT;

              v.address = v.ADDRESS;
            });
            this.data_sgAutocomplete_v2.matchList = matchList;
            this.data_sgAutocomplete_v2.showMoreBtn = matchList.length < d.totalCount;
          },
        },
        this
      );
    },
    readmoreMatch(d) {
      this.currentPage++;
      this.changeMatch();
    },
    search(d) {
      // console.log(`搜索内容`, d, this.inputValue);
    },
  },
};
</script>

应用到了【全网最简短代码】去重对象数组中同一个ID的数据-CSDN博客【代码】【全网最简短代码】去重对象数组中同一个ID的数据。 https://blog.csdn.net/qq_37860634/article/details/146376768