Vue3 + Element Plus 实现强大的图标选择器组件

Vue3 + Element Plus 实现强大的图标选择器组件

引言 📜

在现代Web应用中,图标扮演着至关重要的角色,它们不仅能提升UI的美观度,还能增强用户体验。本文将介绍如何使用Vue3和Element Plus构建一个功能强大的图标选择器组件,该组件集成了Font Awesome图标库,支持三种图标类型(实心、线框、品牌)和搜索功能。

组件功能概述 🔧

  1. 图标预览与清除:在输入框左侧显示当前选中的图标,并可以一键清除
  2. 图标选择对话框:点击预览区域弹出对话框,展示所有图标
  3. 图标分类:通过标签页切换实心图标、线框图标和品牌图标
  4. 搜索功能:通过输入关键词快速过滤图标
  5. 响应式布局:图标列表采用网格布局,自适应宽度

实现流程图 ⚙️

用户点击预览区域
显示图标选择对话框
用户选择标签页类型
加载对应图标集
用户输入搜索词
实时过滤图标
用户选择图标
更新模型值
关闭对话框

实现步骤详解 📝

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图标库,支持三种图标类型和搜索功能。该组件具有以下特点:

  1. 用户友好:清晰的分类和搜索功能让用户快速找到所需图标
  2. 易于集成:使用标准的v-model进行数据绑定
  3. 响应式设计:自适应不同屏幕尺寸
  4. 良好的交互体验:悬停效果和清除功能提升用户体验

可能的扩展方向

  1. 添加收藏功能:允许用户收藏常用图标
  2. 支持自定义图标:除了Font Awesome,还可以集成其他图标库
  3. 增加图标大小和颜色选择:让用户可以直接在组件中调整图标样式
  4. 支持多选模式:允许用户选择多个图标

通过这个组件,开发者可以轻松地在他们的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>
### Vue3 Element Plus `el-select` 选择完成触发事件 在 Vue 3Element Plus 中,`el-select` 是一个常用的选择器组件,允许用户从预定义的选项列表中进行选择。当用户做出选择时,可以通过监听特定事件来执行相应的操作。 #### 使用 `change` 事件 每当用户选择了不同的选项时,会触发 `change` 事件。此事件接收两个参数:当前选中的值以及所有已选中的项(对于多选情况)。下面展示了一个简单的例子说明如何利用该功能: ```html <template> <div> <!-- 定义 el-select 组件 --> <el-select v-model="selectedValue" placeholder="请选择..." @change="handleChange"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <!-- 显示所选内容 --> <p>您选择了: {{ selectedLabel }}</p> </div> </template> <script setup> import { ref, computed } from &#39;vue&#39;; // 初始化数据模型 const selectedValue = ref(&#39;&#39;); const options = [ { value: &#39;option1&#39;, label: &#39;黄金糕&#39; }, { value: &#39;option2&#39;, label: &#39;双皮奶&#39; }, { value: &#39;option3&#39;, label: &#39;蚵仔煎&#39; } ]; // 计算属性获取标签名称 const selectedLabel = computed(() => { const item = options.find(option => option.value === selectedValue.value); return item ? item.label : &#39;&#39;; }); // 处理 change 事件的方法 function handleChange(value) { console.log(&#39;新的选择:&#39;, value); // 打印新选择的值到控制台 } </script> ``` 上述代码片段展示了如何设置 `@change` 监听器并处理其回调函数,在其中可以放置任何想要响应选择改变的操作逻辑[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值