封装一个功能完善的图标选择器Vue组件
在最近开发中,使用到了vue3
开发QMS
后台管理系统,遇到了菜单管理功能,菜单的新增,删除,修改都用到了icon图标选择器,icon
图标选择器能够高效的选择和使用icon
图标,索性将其封装成一个icon
图标选择器组件。一个完善的图标选择器不仅要支持图标的展示和选择,还需要提供搜索、多种视图切换、图标复制等功能。本文将详细介绍如何封装一个功能丰富、用户体验良好的图标选择器组件。
先看效果
功能描述
- 支持
SVG
图标展示:能够动态加载SVG
格式的图标。- 搜索功能:用户可以输入关键词过滤图标。
- 视图切换:支持网格视图和列表视图,用户可以根据需要选择。
- 图标选择与高亮:点击选择图标,组件会高亮显示已选图标,并通过事件通知父组件。
- 复制功能:支持将选中图标的使用代码复制到剪贴板。
- 分页显示:当图标数量较多时,通过分页来优化显示效果。
- 自动获取:自动根据
/src/assets/icons
文件夹中的.svg
图标进行生成。
前期准备
配置vite-plugin-svg-icons
vite-plugin-svg-icons
介绍
vite-plugin-svg-svg-icons
是一个针对 Vite
开发环境的插件,它专门用于优化和简化 SVG
图标的处理。通过使用这个插件,你可以轻松地将 SVG
文件作为组件导入到你的 Vue
、React
或其他基于 Vite
的项目中,从而实现更高效的图标管理。下面是一些该插件的主要作用和特点:
- 自动导入
SVG
文件为组件:你可以将 SVG 文件放在项目中的特定目录下(例如src/assets/icons
),然后通过简单的导入语句在组件中使用这些SVG
文件,无需将它们转换为其他格式或手动编码为组件。 - 优化
SVG
加载:该插件可以帮助减少SVG
文件的体积,例如通过移除元数据、压缩等,从而加快页面加载速度。 - 支持图标精灵
(Icon Sprite)
:插件还可以将多个SVG
图标合并成一个精灵图(sprite)
,这样可以进一步减少HTTP
请求的数量,提高性能。 - 自定义配置:你可以通过配置文件或环境变量来定制插件的行为,比如指定
SVG
文件的目录、是否自动生成组件等。 - 与现代前端框架集成:无论是使用
Vue
、React
还是其他基于Vite
的框架,vite-plugin-svg-icons
都能很好地集成,使得图标管理变得简单。
c
# npm install [email protected] -D --legacy-peer-deps
Vite
配置 vite-plugin-svg-icons
然后,在你的Vite
配置文件(例如vite.config.js
或vite.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/core
的useClipboard
实现图标代码的复制:
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>
使用示例
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);
},
};
- 页面使用
css
<HskIconSelector v-model="form.icon" />
使用效果
总结
通过本文,详细了解了如何封装一个功能完善的图标选择器Vue
组件。这个组件不仅支持SVG
图标的动态加载和展示,还提供了搜索、视图切换、图标选择与高亮显示、复制功能以及分页显示等多种实用功能。无论是从用户体验还是开发效率来看,这个组件都非常值得在项目中使用。如果有更多需求或需要进一步优化,可以根据实际情况扩展和调整这个组件,确保它能够更好地服务于项目需求。 完结~