业务型组件:图标列表组件(IconList)
需求分析
仿照 Element Plus 官网图标页面,实现一个图标列表组件,核心功能:
- 网格展示指定图标集的所有图标
- 每个图标下方显示名称
- 鼠标 hover 高亮
- 点击复制图标名称或 SVG 内容(通过 Switch 切换)
- 复制成功后弹出消息提示
准备图标数据
导出图标名称列表
// scripts/export-icons.ts
// 从 @iconify/json 中导出 Element Plus 图标集的名称列表
import epData from '@iconify/json/json/ep.json'
const iconNames = Object.keys(epData.icons)
console.log(iconNames)
typescript
将输出保存为 JSON 文件供组件使用:
// src/assets/icons/icon-ep.json
[
"add-location",
"aim",
"alarm-clock",
"apple",
"arrow-down",
"arrow-left",
"arrow-right",
"arrow-up",
"... (共约 288 个)"
]
json
完整组件实现
<!-- src/components/IconList/IconList.vue -->
<script setup lang="ts">
import { ref, onBeforeMount } from 'vue'
import { Icon, loadIcon } from '@iconify/vue'
import { useClipboard } from '@vueuse/core'
import { ElMessage, ElSwitch } from 'element-plus'
import iconData from '@/assets/icons/icon-ep.json'
// 复制功能
const source = ref('')
const { copy, copied } = useClipboard({ source })
// 复制模式切换
const copyTypeFlag = ref(true) // true=复制名称, false=复制SVG
// 预加载图标
onBeforeMount(async () => {
const icons = iconData.map((name) => `ep:${name}`)
// loadIcons 会从 Iconify API 批量加载图标数据
const { loadIcons } = await import('@iconify/vue')
await loadIcons(icons)
})
// 短横线转驼峰
function convertString(str: string): string {
return str
.split('-')
.map((word, index) =>
index === 0
? /^[0-9]/.test(word)
? `_${word}`
: word
: word.charAt(0).toUpperCase() + word.slice(1)
)
.join('')
}
// SVG 对象转字符串
function objectToString(svgObj: any): string {
if (!svgObj) return ''
const { body, width = 24, height = 24 } = svgObj
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>`
}
// 点击图标处理
async function handleClick(name: string) {
if (copyTypeFlag.value) {
// 复制图标名称(驼峰形式)
source.value = convertString(name)
} else {
// 复制 SVG 内容
const iconData = await loadIcon(`ep:${name}`)
if (iconData) {
source.value = objectToString(iconData)
}
}
copy(source.value)
}
// 监听复制成功
watch(copied, (val) => {
if (val) {
ElMessage.success('复制成功')
}
})
</script>
<template>
<div class="icon-list-container">
<!-- 顶部控制栏 -->
<div class="flex items-center justify-between mb-4 px-2">
<span class="text-sm text-gray-500">
共 {{ iconData.length }} 个图标
</span>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">复制名称</span>
<ElSwitch v-model="copyTypeFlag" />
<span class="text-sm text-gray-500">复制 SVG</span>
</div>
</div>
<!-- 图标网格 -->
<ul class="flex flex-wrap border border-gray-200 rounded">
<li
v-for="(name, index) in iconData"
:key="index"
class="w-[120px] h-[100px] flex flex-col items-center justify-center
border-r border-b border-gray-200 cursor-pointer
hover:bg-sky-50 transition-colors duration-200"
@click="handleClick(name)"
>
<Icon
:icon="`ep:${name}`"
class="text-3xl py-2"
/>
<span class="text-xs text-gray-500 mt-1 truncate w-full text-center px-1">
{{ convertString(name) }}
</span>
</li>
</ul>
</div>
</template>
vue
使用组件
<!-- src/pages/index.vue -->
<script setup lang="ts">
import IconList from '@/components/IconList/IconList.vue'
</script>
<template>
<div class="p-6">
<h2 class="text-2xl font-bold mb-6">Element Plus 图标集</h2>
<IconList />
</div>
</template>
vue
工具函数详解
短横线转驼峰命名
Element Plus 的图标名使用短横线格式(如 arrow-right),但组件引用时使用驼峰格式(如 ArrowRight):
// src/utils/string.ts
/**
* 将短横线命名转为驼峰命名
* @example
* convertString('arrow-right') // 'arrowRight'
* convertString('arrow-right-board') // 'arrowRightBoard'
* convertString('basketball') // 'basketball'
* convertString('1-abc') // '_1Abc' (数字开头的加下划线前缀)
*/
export function convertString(str: string): string {
return str
.split('-')
.map((word, index) => {
// 数字开头的单词加下划线前缀
if (index === 0 && /^[0-9]/.test(word)) {
return `_${word.charAt(0)}${word.slice(1)}`
}
// 非首个单词首字母大写
if (index > 0) {
return word.charAt(0).toUpperCase() + word.slice(1)
}
return word
})
.join('')
}
typescript
SVG 数据转字符串
loadIcon 返回的是结构化对象,需要转为 SVG 字符串:
// src/utils/svg.ts
interface IconData {
body: string
width?: number
height?: number
left?: number
top?: number
}
/**
* 将 Iconify 图标数据对象转为 SVG 字符串
*/
export function objectToString(data: IconData): string {
if (!data) return ''
const width = data.width ?? 24
const height = data.height ?? 24
const viewBox = `0 0 ${width} ${height}`
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}">${data.body}</svg>`
}
typescript
loadIcon vs getIcon
| API | 加载行为 | 返回值 | 适用场景 |
|---|---|---|---|
loadIcon(name) | 从 API/缓存加载图标 | Promise<IconifyIcon | null> | 需要获取完整 SVG 数据 |
getIcon(name) | 仅从缓存读取 | IconifyIcon | null | 同步获取已缓存的图标 |
loadIcons(names) | 批量加载多个图标 | void(回调通知完成) | 预加载场景 |
loadIcon 在缓存未命中时会发起网络请求;getIcon 仅从内存缓存读取,找不到则返回 null。
关键技术点总结
useClipboard(VueUse)
import { useClipboard } from '@vueuse/core'
const source = ref('')
const { copy, copied } = useClipboard({ source })
// 设置要复制的内容
source.value = '要复制的文本'
// 执行复制
copy(source.value)
// copied 变为 true,可用于显示反馈
typescript
布局方案
采用 Flex 布局 + 固定宽度实现网格效果:
/* 网格布局核心样式 */
.icon-grid {
display: flex;
flex-wrap: wrap;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.icon-item {
width: 120px; /* 固定宽度,自动换行 */
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background-color 0.2s;
}
.icon-item:hover {
background-color: #f0f9ff; /* sky-50 */
}
css
ElMessage 消息提示
import { ElMessage } from 'element-plus'
ElMessage.success('复制成功')
ElMessage.error('复制失败')
ElMessage.warning('请先选择图标')
typescript
扩展作业
在现有基础上增加以下功能:
- 复制组件代码模式:新增 Checkbox,勾选后点击图标复制 Vue 组件代码片段
// 复制格式示例 const code = `<Icon icon="ep:${name}" />`typescript - 图标名称显示/隐藏:通过按钮切换是否显示图标下方的名称文字
- 搜索过滤:添加搜索框,实时过滤图标列表
const searchQuery = ref('') const filteredIcons = computed(() => iconData.filter(name => name.toLowerCase().includes(searchQuery.value.toLowerCase()) ) )typescript - 分类筛选:按图标分类(方向类、提示类、编辑类等)进行筛选
↑