关于dvadmin表情自定义组件分享

发布于 2023-08-14 12:17:14

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

0 条评论

发布
问题