1. emoji-util 的实现
创建文件 srclibsutil.emoji.js
import { qqfaceArr } from '@/components/wx-emoji-picker/utils/util'
const emoji = {}
const emojiUrl = {}
/**
* @description 根据给定参数裁切雪碧图,返回 dataURL
*/
async function loadImageAndClip (value) {
// const SPRITE_URL = require('@/assets/img/qqface_mini.png')
// const TILE_SIZE = 24 // 表情的大小
// const TILE_SPACING_X = 36.5 // 表情横向的间距
// const TILE_SPACING_Y = 36.7 // 表情纵向的间距
const SPRITE_URL = require('@/assets/img/qqface.png')
const TILE_SIZE = 42 // 表情的大小
const TILE_SPACING_X = 65.3 // 表情横向的间距
const TILE_SPACING_Y = 65.6 // 表情纵向的间距
const ROW_COUNT = 8 // 每行的表情数量
const START_OFFSET = 0 // 初始偏移量
// const DESTINATION_SIZE = 24
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 禁用图像平滑
ctx.imageSmoothingEnabled = false
// 设置裁切后的图片大小
canvas.width = TILE_SIZE
canvas.height = TILE_SIZE
// 计算当前表情在雪碧图上的位置
const rowIndex = Math.floor(value / ROW_COUNT)
const colIndex = value % ROW_COUNT
const x = START_OFFSET + colIndex * TILE_SPACING_X
const y = START_OFFSET + rowIndex * TILE_SPACING_Y
// 在 canvas 上绘制所需部分的图片
ctx.drawImage(img, x, y, TILE_SIZE, TILE_SIZE, 0, 0, TILE_SIZE, TILE_SIZE)
// ctx.drawImage(img, x, y, TILE_SIZE, TILE_SIZE, 0, 0, DESTINATION_SIZE, DESTINATION_SIZE)
// 获取 data URL 并返回
resolve(canvas.toDataURL())
}
img.onerror = reject
img.src = SPRITE_URL
})
}
/**
* @description 加载雪碧图指定位置的url
*/
async function loadSpriteData (value) {
// 请填入你的雪碧图加载和裁切逻辑,返回 dataUrl
// 这里是一个伪代码,具体实现依赖于你的需求和实际的加载裁切函数
const dataUrl = await loadImageAndClip(value)
return dataUrl
}
/**
* @description 初始化 emoji 的 url
*/
emoji.initEmojiUrl = async function () {
for (const key in qqfaceArr) {
const value = qqfaceArr[key]
// 获取对应的 dataUrl
const dataUrl = await loadSpriteData(value)
// 存入 url_cache
emojiUrl[value] = dataUrl
}
}
/**
* @description 拿到 emojiUrl 值
* @param {String} key emojiUrl 在 qqfaceArr 中的键
*/
emoji.getEmojiUrl = function (key) {
return emojiUrl[qqfaceArr[key]]
}
emoji.str2html = function (text) {
// 正则表达式匹配 [xxx] 形式的文本
const regex = /\[(.*?)\]/g
// 使用正则替换方法替换匹配的内容
const replacedText = text.replace(regex, (match, p1) => {
// p1 是捕获的表情名称
return `<img src="${this.getEmojiUrl(match)}" width="24px" name="${match}"/>`
})
return replacedText
}
export default emoji
引入到util:
在_srclibsutil.js_调整
import emoji from './util.emoji' // 添加引入
const util = {
cookies,
db,
log,
emoji, // 注册到util
filterParams
}
初始化切片数据:
在 srcinstall.js 后面加入
util.emoji.initEmojiUrl()
2. 自定义组件 wx-emoji-picker 的 index.vue 实现
创建组件文件夹: srccomponentswx-emoji-picker
创建组件: srccomponentswx-emoji-pickerindex.vue
<template>
<div>
<div id="toolbar">
<span class="ql-formats" @click="showEmojiPicker">
<span id="customEmojiBtn" class="ql-custom-emoji">
<img src="./image/smile.svg" alt="Custom Emoji" width="20" height="20">
</span>
</span>
</div>
<quill-editor class="my-quill-editor" v-model="currentValue" :options="editorOption" ref="myQuillEditor" />
<div style="position: relative;">
<div v-show="show_emoji_picker" :style="{ height: height }" class="qqface-container">
<span class="qqface-wrapper" v-for="[key, value] of Object.entries(emoijs)" :key="value">
<img :src="url" class="qqface" :class="[`qqface${value}`]" @click="inputEmoji(key)" />
</span>
</div>
</div>
</div>
</template>
<script>
import util from '@/libs/util'
import { qqfaceArr } from './utils/util.js'
import { quillEditor } from 'vue-quill-editor'
import Quill from 'quill'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
export default {
name: 'wx-emoji-picker',
components: {
quillEditor
},
props: {
value: {
type: String,
required: false
},
readonly: {
type: [Boolean],
required: false,
default: false
},
height: {
type: String,
default: '180px'
}
},
data () {
return {
// 绑定值
currentValue: '',
// 格式白名单
formatWhitelist: ['width', 'name'],
// 配置项:工具栏定义
editorOption: {
formats: this.formatWhitelist,
modules: {
toolbar: {
container: '#toolbar'
}
}
},
emoijs: qqfaceArr,
url: require('@/assets/img/qqface.png'),
url_cache: {},
// 光标最后位置,初始值为0
lastSelection: { index: 0, length: 0 },
// 显示表情框flag
show_emoji_picker: false
}
},
created () {
this.debouncedHandleTextChange = this.debounce(this.handleTextChange, 500) // 500ms 的延迟
this.$nextTick(() => { this.setValue(this.value) })
},
computed: {
},
mounted () {
// 监听光标
this.editor = this.$refs.myQuillEditor.quill // 根据你的ref值调整
this.editor.readonly = this.readonly
this.editor.on('selection-change', (range) => {
if (range) {
this.lastSelection = range
}
})
this.editor.on('text-change', this.debouncedHandleTextChange)
const ImageBlot = Quill.import('formats/image')
class EmojiBlot extends ImageBlot {
static create (value) {
const node = super.create(value.url)
node.setAttribute('width', '24px')
node.setAttribute('name', value.key)
return node
}
}
EmojiBlot.blotName = 'emoji'
EmojiBlot.tagName = 'img'
Quill.register(EmojiBlot)
console.log('value', this.value)
this.lastSelection.index = this.editor.getLength() - 1 >= 0 ? this.editor.getLength() - 1 : 0
},
watch: {
value (value) {
// 父组件收到input事件后会通过v-model改变value参数的值
// 然后此处会watch到value的改变,发出change事件
// change事件放在此处发射的好处是,当外部修改value值时,也能够触发form-data-change事件
this.$emit('change', value)
if (this.html2str(this.currentValue) === value) {
return
}
// // 如果值是被外部改变的,则修改本组件的currentValue
this.setValue(value)
},
lastSelection (newVal) {
console.log(newVal)
}
},
methods: {
// 文本修改 - handle
handleTextChange (delta, oldContents, source) {
// console.log('text-change-innerHtml', this.editor.root.innerHTML)
this.changeValue(this.html2str(this.editor.root.innerHTML))
const currentRange = this.editor.getSelection()
if (currentRange) {
this.lastSelection = currentRange
}
},
// 防抖函数
debounce (func, wait) {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
},
// html 转字符串
html2str (htmlValue) {
let result = htmlValue.replace(/<img[^>]*name="(\[.*?\])"[^>]*>/gi, '$1')
// 将<p>标签内容拆分为数组并连接为带换行符的字符串
result = result.match(/<p>(.*?)<\/p>/gi).map(paragraph => {
return paragraph.replace(/<\/?p>/gi, '')
}).join('\n')
const visibleNewlines = result.replace(/\n/g, '\\n')
console.log('result with visible newlines:', visibleNewlines)
return result
},
// 正则拆分文本 与 emoji样式
splitText (text) {
// 正则表达式匹配 [xxx] 这样的模式,或非[]之间的文本
const regex = /(\[.*?\])|([^[\]]+)/g
// 使用正则表达式匹配文本并返回匹配结果
return text.match(regex)
},
// 判断是否是 emoji 的样式
isEmojiFormat (text) {
return /^\[.*\]$/.test(text)
},
// 将 文本内容 转化为 相应的 Delta
formatContent (strValue) {
const resultList = []
const strList = this.splitText(strValue)
for (const item of strList) {
if (this.isEmojiFormat(item) && this.emoijs[item]) {
console.log('format constent')
const dataUrl = util.emoji.getEmojiUrl(item)
resultList.push({
insert: {
emoji: {
url: dataUrl,
key: item
}
}
})
} else {
resultList.push({ insert: item })
}
}
return resultList
},
setValue (value) {
var content = this.formatContent(value)
this.editor.setContents(content)
},
showEmojiPicker () {
this.show_emoji_picker = !this.show_emoji_picker
},
// 清除 lastSelection 位置数据
deleteText () {
this.editor.deleteText(this.lastSelection.index, this.lastSelection.length)
},
inputEmoji (key) {
console.log('inputEmoji-lastSelection', this.lastSelection)
const imageUrl = util.emoji.getEmojiUrl(key) // 这个函数根据key返回对应的URL
const index = this.lastSelection.index
if (this.lastSelection.length) {
this.deleteText()
}
this.editor.insertEmbed(index, 'emoji', { url: imageUrl, key: key })
this.editor.setSelection(index + 1)
},
// 用户触发按钮点击
changeValue (value) {
// 发出input事件通知父组件,然后请看上面watch的注释 ↑↑↑↑
this.$emit('input', value)
}
}
}
</script>
<style scoped>
.ql-custom-emoji {
cursor: pointer;
}
#toolbar {
height: 36px;
padding: 6px;
}
.ql-formats {
margin-top: -10px;
}
.ql-container.ql-snow {
height: 160px;
overflow-y: auto;
}
.my-quill-editor {
height: 150px;
}
/* 这将设置编辑器内容区域的高度 */
.my-quill-editor .ql-editor {
height: calc(100% - 42px);
/* 减去工具栏的高度 */
overflow-y: auto;
}
</style>
<style lang="scss">
@import '@/assets/style/wx-emoji.scss';
.qqface-container {
overflow-y: scroll;
.qqface-wrapper {
display: inline-block;
transform: scale(1.4);
margin: 12px
}
}
.picker-button {
position: absolute;
right: 20px;
bottom: 20px;
background: #fff;
padding: 10px 20px 4px 20px;
border-radius: 6px;
}
</style>
3. 组件引用
注册组件: srccomponentsindex.js
...
// 添加代码
Vue.component('wx-emoji-picker', () => import('./wx-emoji-picker/index.vue'))
调用组件: (crud.js => column)
{
title: '内容',
key: 'content',
sortable: false,
minWidth: 180,
type: 'textarea',
form: {
// editDisabled: false, 默认false
rules: [
// 表单校验规则
{ required: true, message: '内容必填' }
],
component: {
name: 'wx-emoji-picker',
placeholder: '请输入内容',
span: 24
},
itemProps: {
class: { yxtInput: true }
}
},
component: {
render: (h, scope) => {
const htmlString = util.emoji.str2html(scope.value)
return h('span', { domProps: { innerHTML: htmlString } })
}
}
},
相关资源:
qqface.png
wx-emoji.scss
import { qqfaceArr } from './utils/util.js' 中的util.js