基于vite2.0+vue3.0项目写了一个图标选择器,项目引入对应的css字体文件就行,支持模糊搜索
在这里插入图片描述在这里插入图片描述
项目的文件目录
在这里插入图片描述

1、IconPicker.vue

<template>
  <div class="pp_picker" ref="myRef">
    <div class="inp_box">
      <i class="inp_icon" :class="iconText"></i>
      <input
        class="inp"
        type="text"
        v-model="iconText"
        @focus="data.isShow = true"
        placeholder="请选择图标"
      />
      <i class="inp_close" v-if="iconText" @click="changeIcon()">&times;</i>
    </div>
    <div class="poper" :class="data.cTop" v-if="data.isShow">
      <ul class="pp_list">
        <li v-for="(item, index) in data.newIconList" :key="index">
          <div
            class="pp_box"
            :class="{ active: iconText == item }"
            @click="changeIcon(item, index)"
          >
            <i class="pp_name" :class="item" :title="item"></i>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, toRefs } from "@vue/reactivity";
import initIcon from "@/utils/iconStyle.js";
import {
  onBeforeMount,
  onMounted,
  watchEffect,
} from "@vue/runtime-core";

const props = defineProps({
  icon: {
    type: String,
    default: "",
  },
  highColor: {
    //图标选中状态高亮颜色
    type: String,
    default: "#409eff",
  },
  iconArr: {
    //获取相对应图标的二维数组,例如[[".el-icon-"], [".fa", "fa"], [".icon-", "iconfont"]]
    // 这里默认使用的是element-plus的图标,[[".el-icon-"]]
    type: Array,
    default: [[".el-icon-"]],
  },
});

const myRef = ref(null); //绑定最外层的元素,用于计算位置
const iconText = ref(props.icon.trim()); //重新定义icon的值,input框的双向绑定
const data = reactive({
  allIcon: [], //这个数组用于模糊查询
  newIconList: [], //这个数组用于页面数据遍历
  searchVal: "", //用于搜索
  isShow: false, //显示或隐藏图标选择栏
  cTop: "", //图标选择栏的位置(上面或下面)
});

// 初始化数据,获取css图标
const initFontIconData = () => {
  // 这里有异步操作,不使用data.newIconList=res; 使用push(...res)追加到数组中。
  for (const item of props.iconArr) {
    initIcon(...item).then((res) => {
      data.newIconList.push(...res); //用于遍历数据
      data.allIcon.push(...res); //这个于模糊查询
    });
  }
};

const emit = defineEmits(["update:icon"], ["chooseIcon"]);

// 选择图标和清空input框的值
const changeIcon = (el = "", i) => {
  iconText.value = el;
  emit("update:icon"); //双向绑定,更新父组件的值
  data.newIconList = data.allIcon;
  // data.isShow = false; //关闭图标选择弹窗
};

// 关闭图标选择弹窗
const closeIcon = (e) => {
  if (!myRef.value.contains(e.target)) {
    data.isShow = false;
  }
};

watchEffect(() => {
  data.searchVal = iconText.value;
  // 判断input框的值在不在数组里面,如果在,清空searchVal,不做模糊查询
  if (data.allIcon.indexOf(iconText.value) > -1) {
    data.searchVal = "";
    emit("chooseIcon", iconText.value); //选中图标触发父组件事件
  }
  if (data.searchVal) {
    // 正则的方式匹配数组有searchVal的数据来组成新的数组
    const reg = new RegExp(data.searchVal);
    let arr = [];
    for (let i = 0; i < data.allIcon.length; i++) {
      if (reg.test(data.allIcon[i])) {
        arr.push(data.allIcon[i]);
      }
    }
    data.newIconList = arr;
  }
});

onBeforeMount(() => {
  initFontIconData(); //初始化数据
});

onMounted(() => {
  //点击空白关闭图标选择弹窗,调用closeIcon
  document.addEventListener("click", closeIcon, false);
  
  //图标选择的位置(在input的上面或者下面)
  document.addEventListener("scroll", (e) => {
    // scroll,wheel
    // scroll 滚动条滚动,wheel 鼠标滚动。监听滚动条滚动会好一点
    const dom = myRef.value;
    // console.log(window.pageYOffset);// 滚动条滚动的距离
    let winTop = dom.getBoundingClientRect().top; //div顶部到浏览器窗口顶部的距离
    let winBot = window.innerHeight - dom.getBoundingClientRect().bottom; //div底部到浏览器窗口底部的距离

    if (winBot > 315 || winBot < 0) {
      //当div底部
      data.cTop = "";
    } else {
      if (winTop > 315) {
        data.cTop = "top";
      } else {
        data.cTop = "";
      }
    }
  });
});
</script>

<style lang="scss" scoped>
.pp_picker {
  display: block;
  width: 100%;
  position: relative;
  .inp_box {
    position: relative;
    .inp {
      width: 100%;
      -webkit-appearance: none;
      background-color: transparent;
      border: 1px solid #dcdfe6;
      border-radius: 4px;
      box-sizing: border-box;
      color: #606266;
      display: inline-block;
      height: 34px;
      line-height: 34px;
      outline: 0;
      padding: 0 30px;
      transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
      &:focus {
        border-color: v-bind("props.highColor");
        outline: 0;
      }
    }
    & > i {
      font-style: normal;
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      font-size: 20px;
      line-height: 20px;
      width: 20px;
      height: 20px;
      text-align: center;
    }
    .inp_icon {
      left: 5px;
      z-index: -1;
      font-size: 14px;
    }
    .inp_close {
      display: none;
      z-index: 10;
      cursor: pointer;
      right: 5px;
    }
    &:hover .inp_close,
    .inp:focus + .inp_close {
      display: block;
    }
  }

  .poper {
    position: absolute;
    left: 0;
    top: 100%;
    width: 100%;
    height: 300px;
    z-index: 100;
    background: #fff;
    border: 1px solid #ebeef5;
    border-radius: 4px;
    -webkit-box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
    box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
    color: #606266;
    font-size: 14px;
    line-height: 1.4;
    min-width: 150px;
    padding: 12px;
    margin-top: 15px;
    &::before {
      content: "";
      width: 12px;
      height: 12px;
      box-sizing: border-box;
      position: absolute;
      left: 50%;
      transform: translateX(-50%);
      top: -6px;
      background: #fff;
      -webkit-transform: rotate(45deg);
      transform: rotate(45deg);
      border: 1px solid #ebeef5;
      border-bottom-color: transparent;
      border-right-color: transparent;
    }
    &.top {
      top: -315px;
      margin-top: 0;
      &::before {
        top: auto;
        bottom: -6px;
        -webkit-transform: rotate(225deg);
        transform: rotate(225deg);
      }
    }
  }
}
.pp_list {
  display: flex;
  flex-wrap: wrap;
  margin: 0 -6px;
  max-height: 100%;
  overflow: auto;
  &::-webkit-scrollbar {
    width: 6px;
    border-radius: 4px;
    background: transparent;
  }
  &::-webkit-scrollbar-thumb {
    background: rgba(144, 147, 153, 0.3);
    opacity: 0.5;
    -webkit-box-shadow: 0 0 1px 1px #ccc;
    box-shadow: 0 0 1px 1px #ccc;
    border-radius: 4px;
    &:hover {
      background-color: rgba(144, 147, 153, 0.5);
    }
  }
  li {
    width: 4%;
    padding: 6px;
    .pp_box {
      display: block;
      width: 100%;
      padding-bottom: 80%;
      position: relative;
      border: 1px solid #ddd;
      border-radius: 3px;
      &:hover,
      &.active {
        color: v-bind("props.highColor");
        border-color: v-bind("props.highColor");
      }
      i {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 16px;
        cursor: pointer;
        transition: all 0.3s ease;
      }
    }
    .icon-name {
      display: none;
    }
  }
}
@media screen and (max-width: 1400px) {
  .pp_list li {
    width: 5%;
  }
}
@media screen and (max-width: 1100px) {
  .pp_list li {
    width: calc(100% / 17);
  }
}
@media screen and (max-width: 960px) {
  .pp_list li {
    width: calc(100% / 15);
  }
}
@media screen and (max-width: 768px) {
  .pp_list li {
    width: calc(100% / 12);
  }
}
@media screen and (max-width: 600px) {
  .pp_list li {
    width: 50px;
  }
}
</style>

2、iconStyle.js

封装获取css的方法,也可以直接写到上面的vue组件里面去,这里单独拿出来是以后可能还会用到这个方法,不想使用异步的话,可以直接改掉

/**
 * @function 获取去css图标的class类名
 * @param {string} prefix 区分字体图标的前缀,例如:.icon-
 * @param {string} [fullIcon=''] 是否需要图标的class全名,例如:iconfont
 * @returns {arr} 
 * @example initIcon(".icon-","iconfont").then((res)=>{console.log(res);})
 */
const initIcon = (prefix, fullIcon = '') => {
    return new Promise((resolve, reject) => {
        const regx = /\:before/;   //用于正则匹配有伪类:before的class名
        const styles = document.styleSheets;
        let sheetsIconList = [];
        for (let i = 0; i < styles.length; i++) {
            for (let j = 0; j < styles[i].cssRules.length; j++) {
                let cText = styles[i].cssRules[j].selectorText;
                // 判断是对应前缀开头、是否有::before伪类
                if (
                    cText &&
                    cText.indexOf(prefix) === 0 &&
                    regx.test(cText)
                ) {
                    // 清空前面的.和后面的::before,例如.el-icon-info::before=>el-icon-info
                    // /\./gi  替换.为空。/\:\:before/gi  将::before去除。
                    // /\[data-v-(.+?)\]/gi 
                    let classname = cText.replace(/\./gi, "").replace(/\:\:before/gi, "")
                    // 可能有fa-close,fa-close-o的这种情况
                    if (cText.indexOf(",") > 0) {
                        let m = classname.split(","), m2 = [];
                        if (fullIcon) {
                            for (let a = 0; a < m.length; a++) {
                                m2.push(fullIcon + ' ' + m[a].replace('.', "").trim())
                            }
                        } else {
                            m2 = m;
                        }
                        sheetsIconList.push(...m2);
                    } else {
                        if (fullIcon) {
                            classname = fullIcon + ' ' + classname
                        }
                        sheetsIconList.push(classname);
                    }
                }
            }
        }
        if (sheetsIconList.length > 0) {
            // 得到数组后,先去重一下在将数组返回出去,避免有重复的class
            sheetsIconList = [...new Set(sheetsIconList)]
            resolve(sheetsIconList);
        } else{ 
        	reject("未获取到值,请刷新重试");
        }
    });
}

// 导出方法
export default initIcon;

3、文件调用

<template>
  <div style="height: 120vh; background: #eee"></div>
  <div style="padding: 15px">
    <IconPicker
      v-model:icon="icon_name"
      :iconArr="arr"
      @chooseIcon="getIcon"
    ></IconPicker>
  </div>

  <div style="height: 120vh; background: #eee"></div>
</template>

<script setup>
import { ref, reactive } from "vue";
import IconPicker from "@/components/others/IconPicker.vue";

const arr = ref([[".el-icon-"], [".fa", "fa"], [".icon", "iconfont"]]);
// el-icon-menu
const icon_name = ref("el-icon-coffee");
const getIcon = (e) => {
  console.log(e);
};
</script>

<style>
</style>

组件参数说明

参数说明类型默认值
icon图标的完整class名,例如:fa fa-th-list iconfont icon-menu ;
使用时建议使用v-model:page进行双向绑定
String
highColor图标选中后的高亮颜色String默认为#409eff
iconArr获取相对应图标的二维数组,点击跳到详细说明.Array默认为[[“.el-icon-”]]
element-plus的图标
@chooseIcon图标选中后触发的事件,参数为图标选中后的class

iconArr参数的详细介绍

  首先在项目中引入自己需要的字体css文件,main.js引入、组件内引入、index.html引入等,注意是全局引入
vue项目中按需引入会有[data-v-******]这样的标识,只能在当前组件内使用,

例如在main.js中引入
在这里插入图片描述

数组内容

在这里插入图片描述
  图标区分是采用css字体样式的图标前缀和伪类::before来区分的,但需要注意的是,
[".icon", "iconfont"]这个为例,我在页面写了一个以.icon开头的css样式,含有::before伪类,这样同样也会被当成图标样式提取出来

//这种可能会被当成图标样式
.icon_div1 {
  width: 100%;
  position: relative;
  &::before {
    content: "";
    position: absolute;
    top:0;
    left:0;
  }
}

有问题欢迎大家评论、留言或者私信!

参考案例:图标选择器 - vue-next-admin图标选择组件 - e-icon-picker

Logo

前往低代码交流专区

更多推荐