需求
- Enter发送,Shift + Enter 换行
- 上传图片–插入到输入框
- 粘贴文本及图片
实现代码
chat.vue
<template>
<div>
<!-- 上传图片:选择图片插入到输入框 -->
<div class="tooltip">
<el-tooltip effect="dark" content="只能上传小于2M的图片" placement="top">
<el-upload action="" accept=".jpg, .jpeg, .png" :show-file-list="false" :auto-upload="false" :on-change="getFile">
<i class="sd-image"
</el-upload>
</el-tooltip>
</div>
<!-- 输入框 -->
<div class="content-padding">
<div id="editChat" ref="editChatRef" class="shortMessage-div__input scroll" contenteditable="true"
@paste.prevent="handlePaste" @click="handleSelection" @input="handleSelection" @keydown="keydownEvent" />
</div>
</div>
</template>
<script>
import onPaste from './paste'
export default {
data () {
return {
currentChildNodes: [], //用来存储输入框中子元素
currentSel: null, //用来存储输入框当前的光标对象,用于上传图片
range: null ,//用来存储输入框当前的光标位置,用于上传图片,
limitNum: 10 //消息下行限制数
}
},
mounted () {
if (this.$refs.editChatRef) {
// 默认聚焦,显示光标
this.$refs.editChatRef.focus()
}
},
methods: {
async handlePaste (e) {
const result = await onPaste(e)
if (result.data.size > 2 * 1024 * 1024) {
this.$message.error('图片大小超过2M')
return
}
if (result.type === 'string') {
// 粘贴文本
document.execCommand('insertText', false, result.data)
} else {
// 粘贴图片
const imgRegx = /^data:image/png|jpg|jpeg|gif|svg|bmp|tif/; // 支持的图片格式
if (imgRegx.test(result.compressedDataUrl)) {
// document.execCommand('insertImage', false, result.compressedDataUrl);
const sel = window.getSelection(); // 获取当前光标位置
if (sel && sel.rangeCount === 1 && sel.isCollapsed) {
const range = sel.getRangeAt(0);
const img = new Image();
img.style= 'height: 90px'
img.src = result.compressedDataUrl; // 使用压缩后的图片
range.insertNode(img);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
}
}
},
handleSelection (e) {
// 获取输入框中所有的子元素
if (e.target && e.target.childNodes) {
this.currentChildNodes = e.target.childNodes
}
// 存储输入框最后输入时的光标目标及位置,用于上传图片时,图片插入光标所在位置
this.currentSel = window.getSelection()
this.range = this.currentSel.getRangeAt(0)
},
keydownEvent (e) {
if (!e.shiftKey && e.keyCode === 13) {
e.cancelBubble = true
e.stopPropagation()
e.preventDefault()
this.sendMessage()
return false
}
},
sendMessage () {
if (!this.currentChildNodes.length) {
this.$message.error('消息不能为空')
return
}
let sendMessage = []
// 实现根据输入框内容,汇总消息
let joinFlag = false
this.currentChildNodes.forEach(item => {
if (item.nodeName === '#text' && item.data) {
if (joinFlag) {
sendMessage[sendMessage.length - 1]['text'] += '\n' + item.data
}else {
joinFlag = true
sendMessage.push({
type: 'text',
text: item.data
})
}
} else if (item.localName === 'img') {
joinFlag = false
sendMessage.push({
type: 'image',
text: item.src
})
}
})
// 是否超出消息下行限制数
if (sendMessage.length > 0 && sendMessage.length < this.limitNum) {
sendMessage.forEach(item => {
// 发送消息
this.doSend(item.text, item.type)
})
this.$nextTick(() => {
// 重置剩余可发送消息数;输入框内容
this.limitNum -= sendMessage.length
this.currentChildNodes = []
document.getElementById('editChat').innerHTML = ''
})
} else {
this.$message.error('消息超过下行限制数')
}
}
}
}
</script>
<style scope lang="scss">
.content-padding {
display: flex;
width: 100%;
height: 80%;
.shortMessage-div__input {
width: 100%
}
.scroll {
overflow: auto;
overflow-y: -moz-overlay;
overflow-y: -webkit-overlay;
overflow-y: overlay;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.15);
border-radius: 6px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(0,0,0,0.4);
}
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0);
border-radius: 6px;
}
&::-webkit-scrollbar-track-piece {
background-color: transparent;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(0,0,0,0.4);
}
&::-webkit-scrollbar-thumb {
height: 5px;
width: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(0,0,0,0.3);
border-radius: 6px;
}
}
}
</style>
paste.js
实现获取粘贴的文本及图片
// 定义粘贴函数
const onPaste = (event) => {
// 剪贴板没数据,则直接返回
if (!event.clipboardData || !event.clipboardData.items) {
return;
}
return new Promise((resovle, reject) => {
for(let i = 0, len = event.clipboardData.items.length; i < len; i++) {
const item = event.clipboardData.items[i];
if (item.kind === 'file') {
const file = item.getAsFile();
if (item.type.match('^image/')) {
// 处理图片
handleImage(file, (data) => {
resovle(data)
})
} else {
// 其他文件直接返回
resovle({
data: file,
type: 'file'
})
}
} else if () {
let str = event.clipboardData.getData('text')
resovle({
data: str,
type: 'string'
})
} else {
reject(new Error('不支持粘贴该类型'));
}
}
})
}
function handleImage(file, callback, maxWidth = 200) {
console.log('粘贴的图片', file);
if (!file || !/(?:png|jpg|jpeg|gif)/i.test(file.type)) {
console.log('图片格式不支持');
return;
}
const reader = new FileReader();
reader.onload = function () {
const result = this.result;
console.log('compressedDataUrl', result);
let img = new Image();
img.onload = function() {
let compressedDataUrl = compress(img, file.type, maxWidth, true);
let url = compress(img, file.type, maxWidth, false);
img = null;
callback({
data: file,
compressedDataUrl,
url,
type: 'image'
})
}
img.src = result;
};
reader.readAsDataURL(file);
}
function compress(img, type, maxHeight, flag) {
let canvas = document.createElement('canvas');
let ctx2 = canvas.getContext('2d');
let ratio = img.width / img.height;
let width = img.width, height = img.height;
// 根据flag判断是否压缩图片
if (flag) {
// 压缩后的图片展示在输入框
height = maxHeight;
width = maxHeight * ratio; // 维持图片宽高比
}
canvas.width = width;
canvas.height = height;
ctx2.fillStyle = '#fff';
ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.drawImage(img, 0, 0, width, height);
let base64Data = canvas.toDataURL(type, 0.75);
if (type === 'image/gif') {
let regx = /(?<=data:image).*?(?=;base64)/; // 正则表示时在用于replace时,根据浏览器的不同,有的需要为字符串
base64Data = base64Data.replace(regx, '/gif');
}
canvas = null;
ctx2 = null;
return base64Data;
}
export default onPaste;
getBase64.js
处理上传的图片
function getBase64(file) {
function compress(img, type, maxHeight, flag) {
let canvas = document.createElement('canvas');
let ctx2 = canvas.getContext('2d');
let ratio = img.width / img.height;
let width = img.width, height = img.height;
// 根据flag判断是否压缩图片
if (flag) {
// 压缩后的图片展示在输入框
height = maxHeight;
width = maxHeight * ratio; // 维持图片宽高比
}
canvas.width = width;
canvas.height = height;
ctx2.fillStyle = '#fff';
ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.drawImage(img, 0, 0, width, height);
let base64Data = canvas.toDataURL(type, 0.75);
if (type === 'image/gif') {
let regx = /(?<=data:image).*?(?=;base64)/; // 正则表示时在用于replace时,根据浏览器的不同,有的需要为字符串
base64Data = base64Data.replace(regx, '/gif');
}
canvas = null;
ctx2 = null;
return base64Data;
}
return new Promise(function(resolve, reject) {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function () {
const result = reader.result
let img = new Image()
img.onload = function () {
let compressedDataUrl = compress(img, file.type, 90, true)
let url = compress(img, file.type, 90, false)
img = null;
resolve({
data: file,
compressedDataUrl,
url,
type: 'image'
})
}
img.src = result
},
reader.onerror = function (error) {
reject(error)
}
})
}
注意点
- 图片必须是截图或者打开图片后点复制(图片必须在剪切板中才能复制)
- 一定要设置默认focus,否则上传图片,图片不能插入到输入框
- 前端显示 message 的时候,需要在前端加样式style=“white-space:pre-line”