Vue3 + Element Plus 实现强大的图标选择器组件
引言 📜
在现代Web应用中,图标扮演着至关重要的角色,它们不仅能提升UI的美观度,还能增强用户体验。本文将介绍如何使用Vue3和Element Plus构建一个功能强大的图标选择器组件,该组件集成了Font Awesome图标库,支持三种图标类型(实心、线框、品牌)和搜索功能。
组件功能概述 🔧
- 图标预览与清除:在输入框左侧显示当前选中的图标,并可以一键清除
- 图标选择对话框:点击预览区域弹出对话框,展示所有图标
- 图标分类:通过标签页切换实心图标、线框图标和品牌图标
- 搜索功能:通过输入关键词快速过滤图标
- 响应式布局:图标列表采用网格布局,自适应宽度
实现流程图 ⚙️
实现步骤详解 📝
1. 安装依赖
npm install @fortawesome/vue-fontawesome @fortawesome/fontawesome-svg-core \
@fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons \
@fortawesome/free-brands-svg-icons
2. 组件核心代码解析
图标选择器组件 (web/admin/src/components/fsIconSelector/index.vue
)
<template>
<div class="icon-selector">
<!-- 预览区域 -->
<div class="icon-preview" @click="showIconSelector = true">
<font-awesome-icon v-if="modelValue" :icon="getIconObject(modelValue)" />
<font-awesome-icon v-else :icon="['far', 'circle']" />
<!-- 清除图标 -->
<el-icon v-if="modelValue" class="clear-icon" @click.stop="emit('update:modelValue', '')">
<ele-Close />
</el-icon>
</div>
<!-- 输入框 -->
<el-input :value="modelValue" placeholder="请选择图标" disabled clearable
@clear="emit('update:modelValue', '')"/>
<!-- 图标选择对话框 -->
<el-dialog v-model="showIconSelector" title="选择图标"
:width="dialogWidth || '800px'" append-to-body>
<!-- 搜索框 -->
<div class="icon-search">
<el-input v-model="iconSearch" placeholder="搜索图标" clearable />
</div>
<!-- 标签页 -->
<el-tabs v-model="activeTab" class="icon-tabs">
<!-- 实心图标 -->
<el-tab-pane label="实心图标" name="solid">
<div class="icon-list">
<div v-for="icon in filteredSolidIcons" :key="icon"
class="icon-item" @click="selectIcon(icon)">
<font-awesome-icon :icon="fas[icon]" />
</div>
</div>
</el-tab-pane>
<!-- 线框图标 -->
<el-tab-pane label="线框图标" name="regular">
<div class="icon-list">
<div v-for="icon in filteredRegularIcons" :key="icon"
class="icon-item" @click="selectIcon(icon)">
<font-awesome-icon :icon="far[icon]" />
</div>
</div>
</el-tab-pane>
<!-- 品牌图标 -->
<el-tab-pane label="品牌图标" name="brands">
<div class="icon-list">
<div v-for="icon in filteredBrandIcons" :key="icon"
class="icon-item" @click="selectIcon(icon)">
<font-awesome-icon :icon="fab[icon]" />
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
核心逻辑实现
<script setup lang="ts">
import { ref, computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
const props = defineProps<{
modelValue: string;
dialogWidth?: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const showIconSelector = ref(false);
const iconSearch = ref('');
const activeTab = ref('solid');
// 获取图标对象(重点)
const getIconObject = (iconString: string) => {
if (!iconString) return ['fas', 'question'];
const [prefix, iconName] = iconString.split(' ');
const cleanIconName = iconName.replace('fa-', '');
if (prefix === 'fas') {
return fas[cleanIconName] || ['fas', 'question'];
} else if (prefix === 'far') {
return far[cleanIconName] || ['far', 'question'];
} else {
return fab[cleanIconName] || ['fab', 'question'];
}
};
// 选择图标(重点)
const selectIcon = (icon: string) => {
const prefix = activeTab.value === 'solid' ? 'fas' :
activeTab.value === 'regular' ? 'far' : 'fab';
emit('update:modelValue', `${prefix} fa-${icon}`);
showIconSelector.value = false;
};
// 获取所有唯一图标(重点)
const getUniqueIcons = (iconSet: any) => {
const uniqueIcons = new Set<string>();
const iconNames = new Set<string>();
Object.keys(iconSet).forEach(key => {
if (key === 'prefix' || key === 'iconName') return;
const icon = iconSet[key];
if (icon && typeof icon === 'object' && icon.iconName) {
// 使用 iconName 作为唯一标识
if (!iconNames.has(icon.iconName)) {
iconNames.add(icon.iconName);
uniqueIcons.add(key);
}
}
});
return Array.from(uniqueIcons);
};
const solidIcons = getUniqueIcons(fas);
const regularIcons = getUniqueIcons(far);
const brandIcons = getUniqueIcons(fab);
// 图标搜索过滤(重点)
const filteredSolidIcons = computed(() => {
if (!iconSearch.value) return solidIcons;
const searchTerm = iconSearch.value.toLowerCase();
return solidIcons.filter(icon => {
const iconObj = fas[icon];
return iconObj?.iconName?.toLowerCase().includes(searchTerm);
});
});
// 其他图标类型的过滤实现类似...
</script>
3. 全局注册组件
在 main.ts
中注册组件:
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
import fsIconSelector from './components/fsIconSelector/index.vue';
// 添加所有图标到库中
library.add(fas, far, fab);
// 注册 Font Awesome 组件
app.component('font-awesome-icon', FontAwesomeIcon);
// 注册自定义组件
app.component('FsIconSelector', fsIconSelector);
4. 使用组件
<template>
<el-form-item label="图标" prop="icon">
<fs-icon-selector v-model="formData.icon" />
</el-form-item>
</template>
<script setup>
import { ref } from 'vue';
const formData = ref({
icon: 'fas fa-home' // 默认值
});
</script>
关键实现重点解析 💡
1. 图标唯一性处理
Font Awesome 的图标集合中可能包含多个不同样式的同一图标,我们需要确保每个图标只显示一次:
const getUniqueIcons = (iconSet: any) => {
const uniqueIcons = new Set<string>();
const iconNames = new Set<string>();
Object.keys(iconSet).forEach(key => {
// 排除非图标属性
if (key === 'prefix' || key === 'iconName') return;
const icon = iconSet[key];
if (icon && typeof icon === 'object' && icon.iconName) {
// 使用 iconName 作为唯一标识
if (!iconNames.has(icon.iconName)) {
iconNames.add(icon.iconName);
uniqueIcons.add(key);
}
}
});
return Array.from(uniqueIcons);
};
2. 图标搜索功能
通过计算属性实现实时搜索过滤:
const filteredSolidIcons = computed(() => {
if (!iconSearch.value) return solidIcons;
const searchTerm = iconSearch.value.toLowerCase();
return solidIcons.filter(icon => {
const iconObj = fas[icon];
return iconObj?.iconName?.toLowerCase().includes(searchTerm);
});
});
3. 图标选择逻辑
根据当前激活的标签页确定图标前缀:
const selectIcon = (icon: string) => {
const prefix = activeTab.value === 'solid' ? 'fas' :
activeTab.value === 'regular' ? 'far' : 'fab';
emit('update:modelValue', `${prefix} fa-${icon}`);
showIconSelector.value = false;
};
4. 样式设计要点
.icon-selector {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.icon-preview {
// 预览区域样式
position: relative;
.clear-icon {
// 清除按钮样式
opacity: 0;
transition: opacity 0.3s;
}
&:hover .clear-icon {
opacity: 1;
}
}
}
.icon-list {
// 图标网格布局
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 12px;
min-height: 400px;
height: 400px;
overflow-y: auto;
.icon-item {
// 图标项悬停效果
transition: all 0.3s;
&:hover {
background-color: #f5f7fa;
border-color: #409eff;
}
}
}
效果展示 📸
默认状态
选择器打开状态
总结与扩展 📚
本文介绍了一个功能完善的图标选择器组件的实现,它集成了Font Awesome图标库,支持三种图标类型和搜索功能。该组件具有以下特点:
- 用户友好:清晰的分类和搜索功能让用户快速找到所需图标
- 易于集成:使用标准的v-model进行数据绑定
- 响应式设计:自适应不同屏幕尺寸
- 良好的交互体验:悬停效果和清除功能提升用户体验
可能的扩展方向
- 添加收藏功能:允许用户收藏常用图标
- 支持自定义图标:除了Font Awesome,还可以集成其他图标库
- 增加图标大小和颜色选择:让用户可以直接在组件中调整图标样式
- 支持多选模式:允许用户选择多个图标
通过这个组件,开发者可以轻松地在他们的Vue3项目中添加专业的图标选择功能,提升用户体验和开发效率。
附录 📎
完整代码
<template>
<div class="icon-selector">
<div class="icon-preview" @click="showIconSelector = true">
<font-awesome-icon v-if="modelValue" :icon="getIconObject(modelValue)" />
<font-awesome-icon v-else :icon="['far', 'circle']" />
<el-icon v-if="modelValue" class="clear-icon" @click.stop="emit('update:modelValue', '')">
<ele-Close />
</el-icon>
</div>
<el-input :value="modelValue" placeholder="请选择图标" disabled clearable @clear="emit('update:modelValue', '')"/>
<el-dialog v-model="showIconSelector" title="选择图标" :width="dialogWidth || '800px'" append-to-body>
<div class="icon-search">
<el-input v-model="iconSearch" placeholder="搜索图标" clearable />
</div>
<el-tabs v-model="activeTab" class="icon-tabs">
<el-tab-pane label="实心图标" name="solid">
<div class="icon-list">
<div
v-for="icon in filteredSolidIcons"
:key="icon"
class="icon-item"
@click="selectIcon(icon)"
>
<font-awesome-icon :icon="fas[icon]" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="线框图标" name="regular">
<div class="icon-list">
<div
v-for="icon in filteredRegularIcons"
:key="icon"
class="icon-item"
@click="selectIcon(icon)"
>
<font-awesome-icon :icon="far[icon]" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="品牌图标" name="brands">
<div class="icon-list">
<div
v-for="icon in filteredBrandIcons"
:key="icon"
class="icon-item"
@click="selectIcon(icon)"
>
<font-awesome-icon :icon="fab[icon]" />
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
const props = defineProps<{
modelValue: string;
dialogWidth?: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const showIconSelector = ref(false);
const iconSearch = ref('');
const activeTab = ref('solid');
// 获取图标对象
const getIconObject = (iconString: string) => {
if (!iconString) return ['fas', 'question'];
const [prefix, iconName] = iconString.split(' ');
const cleanIconName = iconName.replace('fa-', '');
if (prefix === 'fas') {
return fas[cleanIconName] || ['fas', 'question'];
} else if (prefix === 'far') {
return far[cleanIconName] || ['far', 'question'];
} else {
return fab[cleanIconName] || ['fab', 'question'];
}
};
// 选择图标
const selectIcon = (icon: string) => {
const prefix = activeTab.value === 'solid' ? 'fas' : activeTab.value === 'regular' ? 'far' : 'fab';
emit('update:modelValue', `${prefix} fa-${icon}`);
showIconSelector.value = false;
};
// 获取所有图标名称
const getUniqueIcons = (iconSet: any) => {
const uniqueIcons = new Set<string>();
const iconNames = new Set<string>();
Object.keys(iconSet).forEach(key => {
if (key === 'prefix' || key === 'iconName') return;
const icon = iconSet[key];
if (icon && typeof icon === 'object' && icon.iconName) {
// 使用 iconName 作为唯一标识
if (!iconNames.has(icon.iconName)) {
iconNames.add(icon.iconName);
uniqueIcons.add(key);
}
}
});
return Array.from(uniqueIcons);
};
const solidIcons = getUniqueIcons(fas);
const regularIcons = getUniqueIcons(far);
const brandIcons = getUniqueIcons(fab);
// 过滤图标
const filteredSolidIcons = computed(() => {
if (!iconSearch.value) return solidIcons;
const searchTerm = iconSearch.value.toLowerCase();
return solidIcons.filter(icon => {
const iconObj = fas[icon];
return iconObj && typeof iconObj === 'object' && iconObj.iconName.toLowerCase().includes(searchTerm);
});
});
const filteredRegularIcons = computed(() => {
if (!iconSearch.value) return regularIcons;
const searchTerm = iconSearch.value.toLowerCase();
return regularIcons.filter(icon => {
const iconObj = far[icon];
return iconObj && typeof iconObj === 'object' && iconObj.iconName.toLowerCase().includes(searchTerm);
});
});
const filteredBrandIcons = computed(() => {
if (!iconSearch.value) return brandIcons;
const searchTerm = iconSearch.value.toLowerCase();
return brandIcons.filter(icon => {
const iconObj = fab[icon];
return iconObj && typeof iconObj === 'object' && iconObj.iconName.toLowerCase().includes(searchTerm);
});
});
</script>
<style scoped lang="scss">
.icon-selector {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.icon-preview {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #f5f7fa;
cursor: pointer;
transition: all 0.3s;
flex-shrink: 0;
position: relative;
&:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
svg {
font-size: 16px;
color: #606266;
}
span {
font-size: 12px;
color: #909399;
}
.clear-icon {
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background-color: #f56c6c;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
}
&:hover .clear-icon {
opacity: 1;
}
}
:deep(.el-input) {
flex: 1;
width: auto;
}
}
.icon-search {
margin-bottom: 16px;
}
.icon-tabs {
:deep(.el-tabs__content) {
padding: 16px 0;
}
}
.icon-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 12px;
min-height: 400px;
height: 400px;
overflow-y: auto;
padding: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
.icon-item {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin: 0 auto;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #f5f7fa;
border-color: #409eff;
}
svg {
font-size: 20px;
color: #606266;
}
}
}
</style>