Vue3后台管理项目封装一个功能完善的图标icon选择器Vue组件动态加载icon文件下的svg图标文件

封装一个功能完善的图标选择器Vue组件

在最近开发中,使用到了vue3开发QMS后台管理系统,遇到了菜单管理功能,菜单的新增,删除,修改都用到了icon图标选择器,icon图标选择器能够高效的选择和使用icon图标,索性将其封装成一个icon图标选择器组件。一个完善的图标选择器不仅要支持图标的展示和选择,还需要提供搜索、多种视图切换、图标复制等功能。本文将详细介绍如何封装一个功能丰富、用户体验良好的图标选择器组件。

先看效果

功能描述

  1. 支持SVG图标展示:能够动态加载SVG格式的图标。
  2. 搜索功能:用户可以输入关键词过滤图标。
  3. 视图切换:支持网格视图和列表视图,用户可以根据需要选择。
  4. 图标选择与高亮:点击选择图标,组件会高亮显示已选图标,并通过事件通知父组件。
  5. 复制功能:支持将选中图标的使用代码复制到剪贴板。
  6. 分页显示:当图标数量较多时,通过分页来优化显示效果。
  7. 自动获取:自动根据/src/assets/icons文件夹中的.svg图标进行生成。

前期准备

配置vite-plugin-svg-icons

vite-plugin-svg-icons介绍

vite-plugin-svg-svg-icons 是一个针对 Vite 开发环境的插件,它专门用于优化和简化 SVG 图标的处理。通过使用这个插件,你可以轻松地将 SVG 文件作为组件导入到你的 VueReact 或其他基于 Vite 的项目中,从而实现更高效的图标管理。下面是一些该插件的主要作用和特点:

  1. 自动导入 SVG 文件为组件:你可以将 SVG 文件放在项目中的特定目录下(例如 src/assets/icons),然后通过简单的导入语句在组件中使用这些 SVG 文件,无需将它们转换为其他格式或手动编码为组件。
  2. 优化 SVG 加载:该插件可以帮助减少 SVG 文件的体积,例如通过移除元数据、压缩等,从而加快页面加载速度。
  3. 支持图标精灵(Icon Sprite):插件还可以将多个 SVG 图标合并成一个精灵图(sprite),这样可以进一步减少 HTTP 请求的数量,提高性能。
  4. 自定义配置:你可以通过配置文件或环境变量来定制插件的行为,比如指定 SVG 文件的目录、是否自动生成组件等。
  5. 与现代前端框架集成:无论是使用 VueReact 还是其他基于 Vite 的框架,vite-plugin-svg-icons 都能很好地集成,使得图标管理变得简单。
c 复制代码
	# npm install [email protected] -D --legacy-peer-deps
Vite 配置 vite-plugin-svg-icons

然后,在你的Vite 配置文件(例如vite.config.jsvite.config.ts)中添加插件配置:

typescript 复制代码
import { mergeConfig } from 'vite';
import eslint from 'vite-plugin-eslint';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; // 修改导入方式
import path from 'node:path';
import process from 'node:process';
import baseConfig from './vite.config.base';

export default mergeConfig(
  {
    mode: 'development',
    base: '/qms-web',
    server: {
      open: true,
      cors: true,
      host: '0.0.0.0',
      fs: {
        strict: true,
      },
      https: false,
      headers: {
        "Access-Control-Allow-Origin": "*", // 避免跨域
      },
      // 新增代理配置
      proxy: {
        '/api/qmsapp': {
          target: 'http://192.168.60.38:32751', // 后端服务地址
          changeOrigin: true,  // 修改请求头中的host值
          logLevel: 'debug',
          // eslint-disable-next-line no-shadow
          rewrite(path: string) {
            return path.replace(/^\/api\/qmsapp/, '');
          }
        }
      }
    },
    plugins: [
      eslint({
        cache: false,
        include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
        exclude: ['node_modules'],
      }),
      createSvgIconsPlugin({ // 使用 createSvgIconsPlugin 函数
        // 指定需要缓存的图标文件夹
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // 指定 symbolId 格式
        symbolId: 'icon-[dir]-[name]',
        svgoOptions: true,
      })
    ],
  },
  baseConfig
);
main.js配置支持svg

通过vite-plugin-svg-icons插件,自动注册项目中所有SVG图标为Vue组件。这使得在应用中可以更方便地使用SVG图标,而无需手动导入每个SVG文件。virtual:svg-icons-register是一个虚拟模块,由插件生成,用于注册所有生成的SVG组件。这种做法简化了SVG图标的管理和使用,提升了开发效率。

typescript 复制代码
// 支持SVG
// eslint-disable-next-line import/no-unresolved
import 'virtual:svg-icons-register'

安装配置sass、sass-loader

c 复制代码
 # npm install sass sass-loader --save-dev --legacy-peer-deps

组件结构

html 复制代码
<template>
  <a-popover trigger="click">
    <a-input
      placeholder="请选择图标"
      :model-value="modelValue"
      allow-clear
      readonly
      @clear="emit('update:modelValue', '')"
    >
      <template #prefix>
        <template v-if="modelValue">
          <HskSvgIcon v-if="modelValue" :size="16" :name="modelValue" />
        </template>
        <icon-search v-else />
      </template>
    </a-input>

    <template #content>
      <div class="container" :class="{ 'is-list': !isGridView }">
        <!-- 搜索栏和视图切换按钮 -->
        <a-row>
          <section style="flex: 1; margin-right: 8px">
            <a-input
              v-model="searchValue"
              placeholder="搜索图标名称"
              allow-clear
              size="small"
              @input="search"
              @clear="search"
            >
              <template #prefix>
                <icon-search />
              </template>
            </a-input>
          </section>

          <a-button size="small" @click="isGridView = !isGridView">
            <template #icon>
              <icon-apps v-if="isGridView" />
              <icon-unordered-list v-else />
            </template>
          </a-button>
        </a-row>

        <!-- 图标列表 -->
        <section class="icon-list">
          <a-row wrap :gutter="4">
            <a-col
              v-for="item of currentPageIconList"
              :key="item"
              :span="isGridView ? 4 : 8"
            >
              <div
                class="icon-item"
                :class="{ active: modelValue === item }"
                @click="handleSelectedIcon(item)"
              >
                <HskSvgIcon :name="item" :size="20"></HskSvgIcon>
                <div class="gi_line_1 icon-name">{{ item }}</div>
              </div>
            </a-col>
          </a-row>
        </section>

        <!-- 分页 -->
        <a-row justify="center" align="center">
          <a-pagination
            size="mini"
            :page-size="pageSize"
            :total="total"
            :show-size-changer="false"
            @change="onPageChange"
          ></a-pagination>
        </a-row>
      </div>
    </template>
  </a-popover>
</template>

核心功能实现

为了动态加载SVG文件,可以使用import.meta.glob来批量导入图标文件。以下是实现方式:

typescript 复制代码
// 提取SVG文件路径,生成图标名称列表
const SvgIconModules = import.meta.glob('@/assets/icons/*.svg');
const iconList: string[] = [];
for (const path in SvgIconModules) {
  const name = path.replace('/src/assets/icons/', '').replace('.svg', '');
  iconList.push(name);
}

HskSvgIcon组件用于渲染单个图标,确保其能够正确显示SVG图标:

typescript 复制代码
<template>
  <svg
    aria-hidden="true"
    :class="svgClass"
    v-bind="$attrs"
    :style="{ color, fill: color, width: iconSize, height: iconSize }"
  >
    <use :xlink:href="iconName"></use>
  </svg>
</template>

搜索功能

typescript 复制代码
const search = () => {
  if (searchValue.value) {
    const temp = searchValue.value.toLowerCase();
    searchList.value = iconList.filter((item) => {
      return item.toLowerCase().includes(temp);
    });
    total.value = searchList.value.length;
    currentPageIconList.value = searchList.value.slice(0, pageSize);
  } else {
    searchList.value = [];
    total.value = iconList.length;
    currentPageIconList.value = iconList.slice(
      (current.value - 1) * pageSize,
      current.value * pageSize
    );
  }
};

视图切换

typescript 复制代码
<div class="container" :class="{ 'is-list': !isGridView }">

.scss样式中定义两种视图的布局:

css 复制代码
.container {
  width: 300px;
  overflow: hidden;
  
  .icon-list {
    margin-top: 10px;
    margin-bottom: 10px;
    
    .icon-item {
      height: 30px;
      margin-bottom: 4px;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      border: 1px dashed var(--color-bg-1);
      
      .icon-name {
        display: none;
      }
      
      &.active {
        border: 1px dashed rgb(var(--primary-3));
        background-color: rgba(var(--primary-6), 0.05);
      }
      
      &:not(.active) {
        &:hover {
          border-color: var(--color-border-3);
        }
      }
    }
  }
}

.is-list {
  min-width: 400px;
  
  .icon-list {
    height: 300px;
    overflow: hidden;
    overflow-y: auto;
    
    .icon-item {
      display: flex;
      flex-direction: row;
      justify-content: flex-start;
      align-items: center;
      padding-left: 4px;
      box-sizing: border-box;
      
      .icon-name {
        margin-left: 6px;
        font-size: 12px;
        color: var(--color-text-2);
        display: block;
      }
    }
  }
}

图标选择与高亮,在用户点击图标时,触发handleSelectedIcon方法:

css 复制代码
const handleSelectedIcon = async (icon: string) => {
  emit('select', icon);
  emit('update:modelValue', icon);
  
  if (props.enableCopy) {
    const { isSupported, copied, copy } = useClipboard();
    if (isSupported) {
      await copy(`<HskSvgIcon :name="${icon}" />`);
      if (copied) {
        Message.success(`已选择并且复制成功 ${icon} 图标`);
      }
    }
  }
};

通过modelValue属性来判断并显示高亮效果:

css 复制代码
<div
  class="icon-item"
  :class="{ active: modelValue === item }"
  @click="handleSelectedIcon(item)"
>

复制功能,使用@vueuse/coreuseClipboard实现图标代码的复制:

css 复制代码
const handleSelectedIcon = async (icon: string) => {
  emit('select', icon);
  emit('update:modelValue', icon);
  
  if (props.enableCopy) {
    const { isSupported, copied, copy } = useClipboard();
    if (isSupported) {
      await copy(`<HskSvgIcon :name="${icon}" />`);
      if (copied) {
        Message.success(`已选择并且复制成功 ${icon} 图标`);
      }
    }
  }
};

分页显示

css 复制代码
const onPageChange = (page: number) => {
  current.value = page;
  
  if (!searchList.value.length) {
    currentPageIconList.value = iconList.slice(
      (page - 1) * pageSize,
      page * pageSize
    );
  } else {
    currentPageIconList.value = searchList.value.slice(
      (page - 1) * pageSize,
      page * pageSize
    );
  }
};

HskIconSelector.vue全部代码

css 复制代码
<template>
  <a-popover trigger="click">
    <a-input
      placeholder="请选择图标"
      :model-value="modelValue"
      allow-clear
      readonly
      @clear="emit('update:modelValue', '')"
    >
      <template #prefix>
        <template v-if="modelValue">
          <HskSvgIcon v-if="modelValue" :size="16" :name="modelValue" />
        </template>
        <icon-search v-else />
      </template>
    </a-input>

    <template #content>
      <div class="container" :class="{ 'is-list': !isGridView }">
        <a-row>
          <section style="flex: 1; margin-right: 8px">
            <a-input
              v-model="searchValue"
              placeholder="搜索图标名称"
              allow-clear
              size="small"
              @input="search"
              @clear="search"
            >
              <template #prefix>
                <icon-search />
              </template>
            </a-input>
          </section>

          <a-button size="small" @click="isGridView = !isGridView">
            <template #icon>
              <icon-apps v-if="isGridView" />
              <icon-unordered-list v-else />
            </template>
          </a-button>
        </a-row>

        <section class="icon-list">
          <a-row wrap :gutter="4">
            <a-col
              v-for="item of currentPageIconList"
              :key="item"
              :span="isGridView ? 4 : 8"
            >
              <div
                class="icon-item"
                :class="{ active: modelValue === item }"
                @click="handleSelectedIcon(item)"
              >
                <!-- {{ item }} -->
                <HskSvgIcon :name="item" :size="20"></HskSvgIcon>
                <div class="gi_line_1 icon-name">{{ item }}</div>
              </div>
            </a-col>
          </a-row>
        </section>

        <a-row justify="center" align="center">
          <a-pagination
            size="mini"
            :page-size="pageSize"
            :total="total"
            :show-size-changer="false"
            @change="onPageChange"
          ></a-pagination>
        </a-row>
      </div>
    </template>
  </a-popover>
</template>

<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { ref } from 'vue';

defineOptions({ name: 'HskIconSelector' });

// eslint-disable-next-line no-use-before-define
const props = withDefaults(defineProps<Props>(), {
  modelValue: '',
  enableCopy: false,
});

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

// 自定义图标模块
const SvgIconModules = import.meta.glob('@/assets/icons/*.svg');

interface Props {
  modelValue?: string;
  enableCopy?: boolean;
}

const searchValue = ref(''); // 搜索词

// 图标列表
const isGridView = ref(false);

const iconList: string[] = [];
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const path in SvgIconModules) {
  const name = path.replace('/src/assets/icons/', '').replace('.svg', '');
  iconList.push(name);
}

const pageSize = 42;
const current = ref(1);
const total = ref(iconList.length); // 图标总数

// 当前页的图标列表
const currentPageIconList = ref(iconList.slice(0, pageSize));
// 搜索列表
const searchList = ref<string[]>([]);

// 页码改变
const onPageChange = (page: number) => {
  current.value = page;
  if (!searchList.value.length) {
    currentPageIconList.value = iconList.slice(
      (page - 1) * pageSize,
      page * pageSize
    );
  } else {
    currentPageIconList.value = searchList.value.slice(
      (page - 1) * pageSize,
      page * pageSize
    );
  }
};

// 搜索
const search = () => {
  if (searchValue.value) {
    const temp = searchValue.value.toLowerCase();
    searchList.value = iconList.filter((item) => {
      return item.toLowerCase().includes(temp);
    });
    total.value = searchList.value.length;
    currentPageIconList.value = searchList.value.slice(0, pageSize);
  } else {
    searchList.value = [];
    total.value = iconList.length;
    currentPageIconList.value = iconList.slice(
      (current.value - 1) * pageSize,
      current.value * pageSize
    );
  }
};

// 点击选择图标
const handleSelectedIcon = async (icon: string) => {
  emit('select', icon);
  emit('update:modelValue', icon);
  if (props.enableCopy) {
    const { isSupported, copied, copy } = useClipboard();
    if (isSupported) {
      await copy(`<${icon} />`);
      if (copied) {
        Message.success(`已选择并且复制成功 ${icon} 图标`);
      }
    }
  }
};
</script>

<style scoped lang="scss">
.container {
  width: 300px;
  overflow: hidden;
  .icon-list {
    margin-top: 10px;
    margin-bottom: 10px;
    .icon-item {
      height: 30px;
      margin-bottom: 4px;
      overflow: hidden;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      border: 1px dashed var(--color-bg-1);
      .icon-name {
        display: none;
      }
      &.active {
        border: 1px dashed rgb(var(--primary-3));
        background-color: rgba(var(--primary-6), 0.05);
      }

      &:not(.active) {
        &:hover {
          border-color: var(--color-border-3);
        }
      }
    }
  }
}

.is-list {
  min-width: 400px;
  .icon-list {
    height: 300px;
    overflow: hidden;
    overflow-y: auto;
    .icon-item {
      display: flex;
      flex-direction: row;
      justify-content: flex-start;
      align-items: center;
      padding-left: 4px;
      box-sizing: border-box;
      .icon-name {
        margin-left: 6px;
        font-size: 12px;
        color: var(--color-text-2);
        display: block;
      }
    }
  }
}
</style>

HskSvgIcon.vue 全部代码

css 复制代码
<template>
  <icon-font :type="iconName" :size="32" />
  <svg
    aria-hidden="true"
    :class="svgClass"
    v-bind="$attrs"
    :style="{ color, fill: color, width: iconSize, height: iconSize }"
  >
    <use :xlink:href="iconName"></use>
  </svg>
</template>

<script setup lang="ts">
import { computed } from 'vue';

defineOptions({ name: 'HskSvgIcon' });

// eslint-disable-next-line no-use-before-define
const props = withDefaults(defineProps<Props>(), {
  name: '',
  color: '',
  size: 20,
});

interface Props {
  name: string;
  color?: string;
  size?: string | number;
}

// 判断传入的值,是否带有单位,如果没有,就默认用px单位
const getUnitValue = (value: string | number): string | number => {
  return /(px|em|rem|%)$/.test(value.toString()) ? value : `${value}px`;
};

const iconSize = computed<string | number>(() => {
  return getUnitValue(props.size);
});

const iconName = computed(() => `#icon-${props.name}`);

const svgClass = computed(() => {
  if (props.name) return `svg-icon icon-${props.name}`;
  return 'svg-icon';
});
</script>

<style scoped lang="scss">
.svg-icon {
  width: auto;
  height: auto;
  // fill: currentColor;
  vertical-align: middle;
  flex-shrink: 0;
}
</style>

使用示例

  1. components/index.ts引入
css 复制代码
import { App } from 'vue';
import { use } from 'echarts/core';
import HskIconSelector from './HskIconSelector/index.vue';
import HskSvgIcon from './HskSvgIcon/index.vue';
export default {
  install(Vue: App) {
    Vue.component('HskIconSelector', HskIconSelector);
    Vue.component('HskSvgIcon', HskSvgIcon);
  },
};
  1. 页面使用
css 复制代码
<HskIconSelector v-model="form.icon" />

使用效果

总结

通过本文,详细了解了如何封装一个功能完善的图标选择器Vue组件。这个组件不仅支持SVG图标的动态加载和展示,还提供了搜索、视图切换、图标选择与高亮显示、复制功能以及分页显示等多种实用功能。无论是从用户体验还是开发效率来看,这个组件都非常值得在项目中使用。如果有更多需求或需要进一步优化,可以根据实际情况扩展和调整这个组件,确保它能够更好地服务于项目需求。 完结~

相关推荐
草明3 分钟前
使用 Chrome Flags 设置(适用于 HTTP 站点开发)
前端·chrome·http
Tz一号1 小时前
前端 git规范-不同软件(GitHub、Sourcetree、WebStorm)、命令行合并方式下增加 --no-ff的方法
前端·git·github
Loadings1 小时前
MCP从理解到实现
前端·cursor·ai 编程
冬冬小圆帽1 小时前
防止手机验证码被刷:React + TypeScript 与 Node.js + Express 的全面防御策略
前端·后端·react.js·typescript
Cmoigo1 小时前
React Native自定义View(Android侧)
前端·react native
LanceJiang2 小时前
文本溢出移入Tooltip提示,我的LeText组件
前端·vue.js
moreface2 小时前
uni.request 配置流式接收+通义千问实现多轮对话
前端·vue.js·人工智能
大元992 小时前
前端必须知道的emoji知识
前端
不服就干2 小时前
js通过游览器启动本地的绿色软件
前端·javascript
零零壹112 小时前
理解Akamai EdgeGrid认证在REST API中的应用
前端·后端