在当今的社交应用、聊天工具或评论系统中,表情符号(Emoji)已成为不可或缺的表达元素。一个好的表情选择器不仅能提升用户体验,还能让交互变得更加生动有趣。今天,我将分享一个我开发的 Vue.js Emoji Picker 组件,它不仅功能齐全,而且具有高度的可定制性。
组件预览与使用场景
想象一下:在您的聊天应用、评论框或任何需要用户输入表情的地方,一个优雅的弹窗徐徐展开,分类清晰的表情符号供用户选择。点击即可插入,操作流畅自然。这正是我们即将构建的组件所能提供的体验。
基本使用示例
javascript
<template>
<div>
<button @click="showPicker = !showPicker">选择表情 😊</button>
<EmojiPicker
:visible="showPicker"
@select="handleEmojiSelect"
/>
</div>
</template>
<script>
import EmojiPicker from './EmojiPicker.vue'
export default {
components: {
EmojiPicker
},
data() {
return {
showPicker: false
}
},
methods: {
handleEmojiSelect(emoji) {
console.log('选中的表情:', emoji)
// 将表情插入输入框等操作
}
}
}
</script>
核心特性解析
1. 智能分类与导航
组件内置10大类表情,涵盖笑脸、手势、动物、食物、活动、符号、节日、自然、物品和交通,总计超过1000个表情符号。
分类导航栏特点:
-
支持三种显示模式:图标+文字、纯图标、纯文字
-
横向滚动设计,适应不同屏幕宽度
-
优雅的滚动条隐藏处理
-
鼠标滚轮支持横向滚动
2. 高度可配置性
通过灵活的props配置,组件可以适应各种使用场景:
javascript
<EmojiPicker
:visible="true"
width="350px"
display-mode="icon"
:visible-categories="['face', 'hand', 'food']"
@select="onSelect"
/>
配置项说明:
-
visible: 控制组件显示/隐藏 -
width: 自定义宽度(支持CSS单位) -
displayMode: 分类显示模式(both|icon|name) -
visibleCategories: 过滤显示指定分类,提升用户效率
3. 优化用户体验的设计细节
横向滚动优化:
javascript
handleWheel(event) {
const container = this.$refs.emojiTabs
const canScrollHorizontally = container.scrollWidth > container.clientWidth
if (canScrollHorizontally) {
event.preventDefault()
const delta = Math.abs(event.deltaY) > Math.abs(event.deltaX)
? event.deltaY
: event.deltaX
container.scrollBy({ left: delta, behavior: 'auto' })
}
}
这个巧妙的方法将垂直滚动手势转换为水平滚动,让用户在使用鼠标滚轮时也能轻松浏览所有分类。
视觉反馈增强:
-
表情悬停时放大效果(
transform: scale(1.3)) -
平滑的过渡动画(
transition: all 0.2s ease) -
清晰的活动状态指示
-
统一的滚动条样式
技术实现亮点
数据结构设计
组件采用分离的数据结构:categories存储分类元数据,emojiCategories存储实际的表情数据。这种设计使得:
-
易于维护和扩展
-
支持灵活的分类过滤
-
保持代码清晰度
计算属性巧妙运用
javascript
filteredCategories() {
if (!this.visibleCategories || this.visibleCategories.length === 0) {
return this.categories.map((cat, index) => ({
...cat,
originalIndex: index
}))
}
return this.categories
.map((cat, index) => ({
...cat,
originalIndex: index
}))
.filter(cat => this.visibleCategories.includes(cat.key))
}
这个计算属性智能处理分类过滤逻辑,确保无论是否配置可见分类,组件都能正常工作。
样式设计哲学
组件的CSS采用现代化设计原则:
-
使用CSS变量友好结构,便于主题定制
-
灵活的盒模型和定位系统
-
响应式设计考虑
-
性能优化的动画和过渡
实际应用建议
1. 性能优化
对于大量表情的渲染,可以考虑虚拟滚动技术。不过当前实现通过分页(分类)已经有效降低了初始渲染压力。
2. 无障碍访问
可以进一步增加ARIA属性,提升屏幕阅读器兼容性:
javascript
<span
class="emoji-item"
role="button"
:aria-label="`选择表情${emoji}`"
tabindex="0"
@click="$emit('select', emoji)"
@keydown.enter="$emit('select', emoji)"
>
{{ emoji }}
</span>
3. 扩展可能性
-
搜索功能:添加表情搜索,快速定位
-
最近使用:记录用户常用表情
-
皮肤色调:支持不同肤色的表情变体
-
自定义表情:集成自定义贴图或GIF
为什么选择这个组件?
-
开箱即用:无需复杂配置,基本功能一应俱全
-
高度可定制:从样式到功能都可按需调整
-
性能优秀:合理的数据结构和渲染优化
-
现代设计:符合当前UI/UX最佳实践
-
易于集成:简单的API设计,快速融入现有项目
组件完整代码
javascript
<template>
<div v-show="visible" class="emoji-picker" :style="{ width: width }">
<!-- 分类导航栏 -->
<div ref="emojiTabs" class="emoji-tabs">
<div
v-for="(category, index) in filteredCategories"
:key="index"
class="tab-item"
:class="{ active: activeCategory === category.originalIndex }"
@click="changeCategory(category.originalIndex)"
>
<template v-if="displayMode === 'both'">
{{ category.icon }} {{ category.name }}
</template>
<template v-else-if="displayMode === 'icon'">
{{ category.icon }}
</template>
<template v-else>
{{ category.name }}
</template>
</div>
</div>
<!-- 表情列表 -->
<div class="emoji-list">
<span
v-for="(emoji, index) in getCurrentEmojis"
:key="index"
class="emoji-item"
@click="$emit('select', emoji)"
>
{{ emoji }}
</span>
</div>
</div>
</template>
<script>
export default {
name: 'EmojiPicker',
props: {
/**
* 是否显示表情选择器
* @type {boolean}
*/
visible: {
type: Boolean,
default: false
},
/**
* 表情选择器宽度
* @type {string}
*/
width: {
type: String,
default: '100%'
},
/**
* 分类显示模式
* @type {string}
* @values 'both' | 'icon' | 'name'
* @default 'both'
*/
displayMode: {
type: String,
default: 'both',
validator: (value) => ['both', 'icon', 'name'].includes(value)
},
/**
* 显示的分类列表(通过 key 控制)
* @type {Array}
* @default null (显示全部)
*/
visibleCategories: {
type: Array,
default: null
}
},
data() {
return {
// 当前选中的分类
activeCategory: 0,
categories: [
{ name: '笑脸', icon: '😊', key: 'face' },
{ name: '手势', icon: '👋', key: 'hand' },
{ name: '动物', icon: '🐶', key: 'animal' },
{ name: '食物', icon: '🍕', key: 'food' },
{ name: '活动', icon: '⚽', key: 'activity' },
{ name: '符号', icon: '❤️', key: 'symbol' },
{ name: '节日', icon: '🎉', key: 'festival' },
{ name: '自然', icon: '☀️', key: 'nature' },
{ name: '物品', icon: '⌚', key: 'object' },
{ name: '交通', icon: '🚗', key: 'travel' }
],
// 按类别整理的表情数据
emojiCategories: [
// 笑脸和情感
[
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
'😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐',
'🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒',
'🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕',
'😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱',
'😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩',
'🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾'
],
// 手势和身体部位
[
'👋', '🤚', '🖐', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆',
'🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️',
'💅', '🤳', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀',
'👁', '👅', '👄', '👶', '🧒', '👦', '👧', '🧑', '👱', '👨', '🧔', '👨🦰', '👨🦱', '👨🦳', '👨🦲', '👩',
'👩🦰', '👩🦱', '👩🦳', '👩🦲', '🧓', '👴', '👵', '🙍', '🙎', '🙅', '🙆', '💁', '🙋', '🧏', '🙇',
'🤦', '🤷', '👮', '🕵', '💂', '🥷', '👷', '🤴', '👸', '👳', '👲', '🧕', '🤵', '👰', '🤰', '🤱'
],
// 动物
[
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', '🐸', '🐵',
'🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗',
'🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', '🦟', '🦗', '🕷', '🦂', '🐢', '🐍', '🦎', '🦖',
'🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆'
],
// 食物和饮料
[
'🍇', '🍈', '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑', '🍒', '🍓', '🫐', '🥝',
'🍅', '🫒', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽', '🌶', '🫑', '🥒', '🥬', '🥦', '🧄', '🧅', '🍄',
'🥜', '🍞', '🥐', '🥖', '🫓', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔',
'🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🫔', '🥙', '🧆', '🥚', '🍳', '🥘', '🍲', '🫕', '🥣', '🥗',
'🍿', '🧈', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥',
'🥮', '🍡', '🥟', '🥠', '🥡', '🦀', '🦞', '🦐', '🦑', '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂',
'🍰', '🧁', '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🫖', '🍵', '🍶', '🍾', '🍷',
'🍸', '🍹', '🍺', '🍻', '🥂', '🥃', '🥤', '🧋', '🧃', '🧉', '🧊', '🥢', '🍽', '🍴', '🥄', '🔪',
'🏺'
],
// 活动和运动
[
'⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍',
'🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', '🎽', '🛹', '🛼', '⛸', '🥌', '🎿',
'⛷', '🏂', '🪂', '🏋', '🤼', '🤸', '⛹', '🤺', '🤾', '🏌', '🏇', '🧘', '🏄', '🏊', '🤽', '🚣',
'🧗', '🚵', '🚴', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖', '🏵', '🎗', '🎫', '🎟', '🎪', '🤹', '🎭',
'🩰', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🎸', '🪕', '🎻', '🎲', '♟',
'🎯', '🎳', '🎮', '🎰', '🧩'
],
// 符号和图标
[
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖',
'💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈',
'♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '🆔', '⚛️', '🉑', '☢️', '☣️',
'📴', '📳', '🈶', '🈚', '🈸', '🈺', '🈷️', '✴️', '🆚', '💮', '🉐', '㊙️', '㊗️', '🈴', '🈵', '🈹',
'🈲', '🅰️', '🅱️', '🆎', '🆑', '🅾️', '🆘', '❌', '⭕', '🛑', '⛔', '📛', '🚫', '💯', '💢', '♨️',
'🚷', '🚯', '🚳', '🚱', '🔞', '📵', '🚭', '❗', '❕', '❓', '❔', '‼️', '⁉️', '🔅', '🔆', '〽️',
'⚠️', '🚸', '🔱', '⚜️', '🔰', '♻️', '✅', '🈯', '💹', '❇️', '✳️', '❎', '🌐', '💠', 'Ⓜ️', '🌀',
'💤', '🏧', '🚾', '♿', '🅿️', '🈳', '🈂️', '🛂', '🛄', '🚹', '🚺', '🚻', '🚼',
'⚧️', '🚮', '📶', '🈁', '🔣', 'ℹ️', '🔤', '🔡', '🔠', '🆖', '🆗', '🆙', '🆒', '🆕', '🆓',
'0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔢', '#️⃣', '*️⃣'
],
// 节日和特殊场合
[
'🎉', '🎊', '🎁', '🎀', '🎃', '🎄', '🎆', '🎇', '🧨', '✨', '🎈', '🎓', '🎖'
],
// 天气和自然
[
'☀️', '🌤', '⛅', '🌥', '☁️', '🌦', '🌧', '⛈', '🌩', '⚡', '❄️', '☃️', '⛄', '☄️', '🔥', '💧',
'🌊', '🌙', '🌍', '🌎', '🌏', '🪐', '🌟', '🌠', '🌌', '🌈', '🌪', '🌫', '🌬', '💨', '💫', '💦', '☔'
],
// 物品和工具
[
'⌚', '📱', '📲', '💻', '⌨️', '🖥', '🖨', '🖱', '🖲', '🕹', '🗜', '💾', '💿', '📼', '📷',
'📸', '📹', '🎥', '📽', '🎞', '📞', '☎️', '📠', '📺', '📻', '🎙', '🎚', '🎛', '🧭',
'⏱', '⏲', '⏰', '🕰', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯', '🪔', '🧯', '🛢', '💸',
'💵', '💴', '💶', '💷', '💰', '💳', '💎', '⚖️', '🔧', '🔨', '⚒', '🛠', '⛏', '🔩', '⚙️',
'🗜', '🦯', '🔗', '⛓', '🪝', '🧰', '🧲', '🪜', '⚗️', '🧪', '🧫', '🧬', '🔬', '🔭', '💉',
'🩸', '💊', '🩹', '🩺', '🚪', '🛗', '🪞', '🪟', '🛏', '🛋', '🪑', '🚽', '🪠', '🚿', '🛁', '🪤',
'🧴', '🧷', '🧹', '🧺', '🧻', '🪣', '🧼', '🪥', '🧽', '🛒', '🚬', '⚰️', '🪦', '⚱️'
],
// 交通和地图
[
'🚗', '🚕', '🚙', '🚌', '🚎', '🏎', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚', '🚛', '🚜', '🦽',
'🦼', '🛴', '🚲', '🛵', '🏍', '🛺', '🚨', '🚔', '🚍', '🚘', '🚖', '🚡', '🚠', '🚟', '🚃', '🚋',
'🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', '🚇', '🚊', '🚉', '✈️', '🛫', '🛬', '🛩', '💺', '🛰',
'🚀', '🛸', '🚁', '🛶', '⛵', '🚤', '🛥', '🛳', '⛴', '🚢', '⚓', '🪝', '⛽', '🚧', '🚦', '🚥',
'🚏', '🗺', '🗿', '🗽', '🗼', '🏰', '🏯', '🏟', '🎡', '🎢', '🎠', '⛲', '⛱', '🏖', '🏝', '🏜',
'🏞', '🏠', '🏡', '🏢', '🏣', '🏤', '🏥', '🏦', '🏪', '🏫', '🏩', '💒', '🏛', '⛪', '🕌', '🕍',
'🕋', '⛩'
]
]
}
},
computed: {
getCurrentEmojis() {
return this.emojiCategories[this.activeCategory]
},
/**
* 过滤后的分类列表
*/
filteredCategories() {
if (!this.visibleCategories || this.visibleCategories.length === 0) {
// 如果没有配置,显示全部分类,并添加原始索引
return this.categories.map((cat, index) => ({
...cat,
originalIndex: index
}))
}
// 根据配置过滤分类
return this.categories
.map((cat, index) => ({
...cat,
originalIndex: index
}))
.filter(cat => this.visibleCategories.includes(cat.key))
}
},
mounted() {
// 添加鼠标滚轮事件监听,实现横向滚动
this.$refs.emojiTabs.addEventListener('wheel', this.handleWheel, { passive: false })
},
beforeDestroy() {
// 移除事件监听
if (this.$refs.emojiTabs) {
this.$refs.emojiTabs.removeEventListener('wheel', this.handleWheel)
}
},
methods: {
changeCategory(index) {
this.activeCategory = index
},
/**
* 处理鼠标滚轮事件,实现横向滚动
*/
handleWheel(event) {
const container = this.$refs.emojiTabs
if (!container) return
// 检查是否可以横向滚动
const canScrollHorizontally = container.scrollWidth > container.clientWidth
if (canScrollHorizontally) {
// 阻止默认的纵向滚动
event.preventDefault()
// 将纵向滚动转换为横向滚动,使用 scrollBy 方法更平滑
const delta = Math.abs(event.deltaY) > Math.abs(event.deltaX) ? event.deltaY : event.deltaX
container.scrollBy({
left: delta,
behavior: 'auto'
})
}
}
}
}
</script>
<style scoped>
.emoji-picker {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
margin-bottom: 8px;
}
.emoji-tabs {
display: flex;
flex-wrap: nowrap; /* 防止换行,确保横向滚动 */
border-bottom: 1px solid #eee;
padding: 8px 10px;
background-color: #f9f9f9;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
overflow-x: auto;
overflow-y: hidden;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.emoji-tabs::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.tab-item {
padding: 6px 12px;
cursor: pointer;
border-radius: 4px;
margin-right: 4px;
font-size: 14px;
color: #666;
transition: all 0.2s ease;
flex-shrink: 0; /* 防止压缩 */
white-space: nowrap; /* 防止换行 */
}
.tab-item.active {
background-color: #fff;
color: #409eff;
border: 1px solid #ddd;
border-bottom: none;
margin-bottom: -1px;
}
.tab-item:hover {
background-color: #f0f0f0;
}
.emoji-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px;
max-height: 250px;
overflow-y: auto;
}
.emoji-item {
font-size: 18px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.emoji-item:hover {
background-color: #f0f0f0;
transform: scale(1.3);
}
/* 统一滚动条样式 */
.emoji-list::-webkit-scrollbar {
width: 6px;
}
.emoji-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.emoji-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.emoji-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
结语
这个Emoji Picker组件是我在实际项目中提炼和优化的成果,它平衡了功能丰富性和使用简洁性。无论是快速原型开发还是生产级应用,它都能提供可靠的表情选择解决方案。
组件代码完全开源,您可以根据自己的需求进一步修改和扩展。在日益重视用户体验的今天,一个好的表情选择器虽小,却能显著提升用户的使用愉悦度。
希望这个组件能对您的项目有所帮助!如果您有任何改进建议或使用案例分享,欢迎交流讨论。