Skip to content

实现 Emoji 选择器 工具

分类:developer | 标签:emoji、表情、复制 分类浏览并复制 Emoji

2.1 功能说明

分类浏览并复制 Emoji

2.1.1 使用指南

功能说明

按分类浏览 Emoji,支持中文关键词搜索,点击即复制到剪贴板,并记录最近使用。

使用场景

评论、社交文案、README、提交信息中快速插入表情符号。

小提示

若个别表情显示为方块,说明当前系统字体缺少该字符(多为较新版本 Unicode),复制出去在支持的设备上仍可正常显示。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • nameOf()
  • loadRecent()
  • pushRecent()
  • clearRecent()
  • pick()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, computed, onMounted } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'

// 注意:Emoji 多为 UTF-16 代理对(4 字节),禁止用 str.split('') 拆分,
// 否则会被拆成孤立代理字符而渲染成「?」。此处直接以数组逐个声明。
const CATEGORIES = [
  {
    name: '表情',
    list: [
      ['😀', '大笑'], ['😁', '露齿笑'], ['😂', '笑哭'], ['🤣', '笑翻'],
      ['😊', '微笑'], ['🙂', '轻微笑'], ['😉', '眨眼'], ['😍', '花痴'],
      ['😘', '飞吻'], ['😎', '墨镜酷'], ['🤔', '思考'], ['🤗', '拥抱'],
      ['😴', '睡觉'], ['😭', '大哭'], ['😢', '流泪'], ['😡', '生气'],
      ['🥳', '庆祝'], ['😇', '天使'], ['🤩', '星星眼'], ['😅', '尴尬'],
      ['😏', '得意'], ['😱', '尖叫'], ['🤯', '爆炸头'], ['😐', '面无表情'],
      ['🙄', '白眼'], ['😜', '吐舌'], ['🤮', '呕吐'], ['🥺', '可怜'],
      ['😷', '口罩'], ['🤡', '小丑']
    ]
  },
  {
    name: '手势',
    list: [
      ['👍', '点赞'], ['👎', '点踩'], ['👏', '鼓掌'], ['🙏', '拜托 祈祷'],
      ['💪', '肌肉 加油'], ['🤝', '握手'], ['✌️', '胜利'], ['🤞', '交叉手指'],
      ['👌', 'OK'], ['🤙', '打电话'], ['👋', '挥手 再见'], ['🤟', '爱你'],
      ['👊', '拳头'], ['✊', '举拳'], ['👈', '左指'], ['👉', '右指'],
      ['👆', '上指'], ['👇', '下指'], ['🖐️', '张开手'], ['🤲', '捧手']
    ]
  },
  {
    name: '动物',
    list: [
      ['🐶', '狗'], ['🐱', '猫'], ['🐭', '老鼠'], ['🐹', '仓鼠'],
      ['🐰', '兔子'], ['🦊', '狐狸'], ['🐻', '熊'], ['🐼', '熊猫'],
      ['🐨', '考拉'], ['🐯', '老虎'], ['🦁', '狮子'], ['🐮', '牛'],
      ['🐷', '猪'], ['🐸', '青蛙'], ['🐵', '猴子'], ['🐔', '鸡'],
      ['🐧', '企鹅'], ['🐦', '鸟'], ['🦄', '独角兽'], ['🐝', '蜜蜂'],
      ['🐢', '乌龟'], ['🐟', '鱼'], ['🐬', '海豚'], ['🐳', '鲸鱼']
    ]
  },
  {
    name: '食物',
    list: [
      ['🍎', '苹果'], ['🍊', '橘子'], ['🍋', '柠檬'], ['🍉', '西瓜'],
      ['🍇', '葡萄'], ['🍓', '草莓'], ['🍒', '樱桃'], ['🍑', '桃子'],
      ['🥭', '芒果'], ['🍍', '菠萝'], ['🍌', '香蕉'], ['🥑', '牛油果'],
      ['🍔', '汉堡'], ['🍟', '薯条'], ['🍕', '披萨'], ['🌭', '热狗'],
      ['🍿', '爆米花'], ['🍩', '甜甜圈'], ['🍪', '饼干'], ['🎂', '蛋糕'],
      ['🍚', '米饭'], ['🍜', '面条'], ['🍣', '寿司'], ['☕', '咖啡'],
      ['🍺', '啤酒'], ['🍵', '茶']
    ]
  },
  {
    name: '自然',
    list: [
      ['☀️', '太阳 晴'], ['🌙', '月亮'], ['⭐', '星星'], ['🌟', '闪耀'],
      ['💫', '眩晕星'], ['🌈', '彩虹'], ['🔥', '火'], ['🌊', '海浪 水'],
      ['🌸', '樱花'], ['🌺', '花'], ['🌲', '松树'], ['🌳', '树'],
      ['🌴', '椰树'], ['☁️', '云'], ['⚡', '闪电'], ['❄️', '雪花'],
      ['🌍', '地球'], ['🌧️', '下雨'], ['🍀', '四叶草'], ['🌵', '仙人掌']
    ]
  },
  {
    name: '符号',
    list: [
      ['✅', '对勾 通过'], ['❌', '叉 错误'], ['⚠️', '警告'], ['❓', '问号'],
      ['❗', '感叹号'], ['💡', '灯泡 想法'], ['🔔', '铃铛 通知'], ['📌', '图钉'],
      ['📎', '回形针'], ['✏️', '铅笔'], ['📚', '书'], ['💻', '电脑'],
      ['📱', '手机'], ['🔑', '钥匙'], ['💰', '钱'], ['❤️', '红心 爱'],
      ['💔', '心碎'], ['🔒', '锁'], ['🔍', '搜索 放大镜'], ['🗑️', '垃圾桶 删除'],
      ['⭕', '圈'], ['🚫', '禁止'], ['♻️', '回收'], ['🔗', '链接']
    ]
  },
  {
    name: '活动',
    list: [
      ['⚽', '足球'], ['🏀', '篮球'], ['🏈', '橄榄球'], ['⚾', '棒球'],
      ['🎾', '网球'], ['🏐', '排球'], ['🎱', '台球'], ['🎮', '游戏'],
      ['🎯', '靶心 目标'], ['🎲', '骰子'], ['🎸', '吉他'], ['🎺', '小号'],
      ['🎬', '电影'], ['🎨', '画画'], ['🚀', '火箭'], ['✈️', '飞机'],
      ['🚗', '汽车'], ['🏠', '房子'], ['🎉', 'party 庆祝'], ['🎁', '礼物'],
      ['🏆', '奖杯'], ['🥇', '金牌'], ['🎤', '麦克风'], ['📷', '相机']
    ]
  }
]

const RECENT_KEY = 'emoji-picker-recent'
const RECENT_MAX = 24

const cat = ref(CATEGORIES[0].name)
const keyword = ref('')
const copied = ref('')
const recent = ref([])

const nameMap = new Map()
CATEGORIES.forEach(c => c.list.forEach(([e, n]) => nameMap.set(e, n)))

function nameOf(e) {
  return nameMap.has(e) ? nameMap.get(e) : ''
}

const currentList = computed(() => {
  const found = CATEGORIES.find(c => c.name === cat.value)
  return found ? found.list : []
})

const searchResult = computed(() => {
  const kw = keyword.value.trim().toLowerCase()
  if (!kw) return []
  const out = []
  CATEGORIES.forEach(c => {
    c.list.forEach(item => {
      if (item[1].toLowerCase().includes(kw) || c.name.includes(kw) || item[0] === kw) {
        out.push(item)
      }
    })
  })
  return out
})

function loadRecent() {
  const raw = localStorage.getItem(RECENT_KEY)
  if (raw === null) return
  try {
    const arr = JSON.parse(raw)
    if (Array.isArray(arr)) {
      recent.value = arr.filter(x => typeof x === 'string' && x.length > 0).slice(0, RECENT_MAX)
    }
  } catch {
    // 本地数据损坏时直接忽略,不影响功能
    localStorage.removeItem(RECENT_KEY)
  }
}

function pushRecent(e) {
  const next = [e, ...recent.value.filter(x => x !== e)].slice(0, RECENT_MAX)
  recent.value = next
  localStorage.setItem(RECENT_KEY, JSON.stringify(next))
}

function clearRecent() {
  recent.value = []
  localStorage.removeItem(RECENT_KEY)
}

async function pick(e) {
  if (navigator.clipboard && window.isSecureContext) {
    await navigator.clipboard.writeText(e)
  } else {
    // 非安全上下文(http 访问)没有 Clipboard API,退回 textarea + execCommand
    const ta = document.createElement('textarea')
    ta.value = e
    ta.style.position = 'fixed'
    ta.style.opacity = '0'
    document.body.appendChild(ta)
    ta.select()
    document.execCommand('copy')
    document.body.removeChild(ta)
  }
  pushRecent(e)
  copied.value = e
  setTimeout(() => { if (copied.value === e) copied.value = '' }, 1500)
}

onMounted(loadRecent)

2.3 效果截图

Emoji 选择器 效果截图

工具访问地址:https://www.i91tools.com/tools/emoji-picker