本期完成了:
1、心跳检测
2、支持发送表情与图片【这个目前还需要优化下,当图片上传后会被默认选中,需要点击一下旁边,使之失去选中效果,才能正常,留待下期优化吧】
3、新增了一些细节,消息固定位置,是否显示呢称,新消息来的闪烁提示等
4、将整个模块独立了出来,在博客页新增了全局挂载
5、如何处理图片的加载无法正常获取到准确高度,导致无法滚动到准确位置(这个是这期本人觉得最复杂的,等待图片加载完再获取高度,体验太差;设置固定高度,又无法兼容到小图片,大的图片有看不清;具体解决方案在下方)
下期功能
1、文件发送
2、在线语音
感兴趣的,可以私聊我,也可以点个收藏,关注,以下是核心代码示例
心跳检测
为何需要做这个,长连接不稳定,会自动断开,需要我们手动来做在线检测和重连
js
// 轮询心跳检测
setHeartBeat() {
let { room_id } = this
clearTimeout(this.timer)
this.timer = setTimeout(() => {
im_heart({ room_id })
.then((res) => {
if (res && this.isObject(res.data) && this.isObject(res.data.data)) {
let { status } = res.data.data
if (status == 2) {
console.log('您已掉线,开始重新加入')
this.socket.emit('join_room', {
room_id,
user_id: this.userdata._id,
})
}
this.setHeartBeat()
}
})
.catch(()=>{})
}, this.heartTime)
},
支持发送表情与图片
这个使用了高级css3属性来完成
html
<div
:id="myInputId"
class="im-input kl-contenteditable-input flex-1 no-select f-14"
contenteditable="true"
@paste="pasteEvent($event)"
@blur="blurEvent"
></div>
js
async pasteEvent(event) {
// 尝试从 event.clipboardData 获取粘贴的项
if (event.clipboardData && event.clipboardData.items) {
for (let index in event.clipboardData.items) {
const item = event.clipboardData.items[index]
if (item.kind === 'file') {
event.preventDefault()
// 文件类型,将数据收集为fil
let file = item.getAsFile()
try {
let {
file: miniFile,
newWidth,
newHeight,
} = await this.compressImg(file, 0.85)
const formData = new FormData()
formData.append('file', miniFile)
const devicePixelRatioa = window.devicePixelRatio || 1
// 上传图片,同时需要上传图片的宽高
upload_imgs_im(formData, {
type: this.isIm ? 'im' : 'sys',
devicePixelRatioa,
width: Math.floor(newWidth / devicePixelRatioa),
height: Math.floor(newHeight / devicePixelRatioa),
}).then((res) => {
if (res.code != 200) {
return this.$message.error(res.msg || '请重新上传')
}
// 将返回的图片链接替换到输入框中
let imgUrl = baseURL + this.filePath + res.data[0]?.filename
this.textContent = `<img src="${imgUrl}" class="contenteditable-unpload-img" />`
this.insertHtmlAtCaret(this.textContent)
})
} catch (err) {
this.$message.warning('请重新上传')
}
}
}
}
},
insertHtmlAtCaret(html, element = document.querySelector('.my-input')) {
// 获取当前元素的选中范围
let range, selection
if (window.getSelection) {
// 大多数浏览器,包括IE9+
selection = window.getSelection()
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0)
} else {
// 如果没有选中范围,则创建一个新的范围
range = document.createRange()
range.selectNodeContents(element)
range.collapse(false) // 将范围设置在元素内容的末尾
selection.addRange(range)
}
} else if (document.selection && document.selection.createRange) {
// 旧版本的IE
range = document.selection.createRange()
}
// 删除选中范围的内容(如果有的话)
if (range) {
range.deleteContents()
// 创建一个临时元素来保存HTML
const tempEl = document.createElement('div')
tempEl.innerHTML = html
// 将临时元素的内容复制到范围中
while (tempEl.firstChild) {
range.insertNode(tempEl.firstChild)
}
}
},
如何解决图片高度问题
前端部分
上传:可以看到我们在上传图片时同时上传了图片的高度与宽度
js
// 上传图片,同时需要上传图片的宽高
upload_imgs_im(formData, {
type: this.isIm ? 'im' : 'sys',
devicePixelRatioa,
width: Math.floor(newWidth / devicePixelRatioa),
height: Math.floor(newHeight / devicePixelRatioa),
}).then((res) => {
if (res.code != 200) {
return this.$message.error(res.msg || '请重新上传')
}
// 将返回的图片链接替换到输入框中
let imgUrl = baseURL + this.filePath + res.data[0]?.filename
this.textContent = `<img src="${imgUrl}" class="contenteditable-unpload-img" />`
this.insertHtmlAtCaret(this.textContent)
})
} catch (err) {
this.$message.warning('请重新上传')
}
回显:直接读取链接上的宽高,来计算出需要呈现的最终宽高,这样就可以不用等到图片加载完毕,就能自动滚动到准确位置
js
mounted() {
let { chatItemClassName,maxWidth } = this
let imgs = document.querySelectorAll(`.${chatItemClassName} .contenteditable-unpload-img`)
if (imgs && imgs.length > 0) {
for (let i = 0; i < imgs.length; i++) {
const item = imgs[i]
item.onclick = () => {
this.prevewImg(item)
}
// 重新设置图片的宽高
const src = $(item).attr('src')
let arr = src.split('~')
arr = arr.filter((item) => !isNaN(+item))
if (Array.isArray(arr)) {
let len = arr.length
if (len === 3) {
let width = +arr[1]
let height = +arr[2]
if (isNaN(width) || isNaN(height)) return
if (width > maxWidth) {
let scale = maxWidth / width
width = maxWidth
height = height * scale
}
$(item).css({
width: width + 'px',
height: height + 'px',
})
}
}
}
}
},
express的上传代码
这边我们需要接收宽高,并将宽高信息放到文件名上
js
const path = require("path");
const multer = require("multer");
module.exports = (limit = 1, file_type_name = "blog") => {
let storage = multer.diskStorage({
destination: function (req, file, cb) {
let { type } = req.query;
if (type) {
file_type_name = type;
}
const file_path = path.resolve(
__dirname,
"../../public/",
file_type_name
);
cb(null, file_path);
},
filename: function (req, file, cb) {
let { user_id, devicePixelRatioa = 1, width = 0, height = 0 } = req.query;
let fileOption = {
author_id: user_id,
netdisk_url: "",
netdisk_name: "",
netdisk_save_name: "",
netdisk_size: "",
netdisk_create_time: "",
};
let fileFormat = file.originalname.split(".");
let old_name = "";
fileFormat.forEach((item, index) => {
if (index < fileFormat.length - 1) {
old_name += item;
}
});
let file_type = fileFormat[fileFormat.length - 1];
let netdisk_save_name = `${old_name}-${Date.now()}~${devicePixelRatioa}~`;
if (width && height) {
netdisk_save_name += `${width}~${height}~`;
}
netdisk_save_name += `.${file_type}`;
// 存储相关数据到自定义项
fileOption.netdisk_url = file_type_name + "/" + netdisk_save_name;
fileOption.netdisk_name = file.originalname || "";
fileOption.netdisk_save_name = netdisk_save_name || "";
fileOption.netdisk_size = file.size || 0;
fileOption.netdisk_create_time = Date.now();
req.fileOption = fileOption;
cb(null, netdisk_save_name);
},
});
let upload = multer({ storage: storage });
// file 前端上传key也必须 都是 file
let result = upload.array("file", limit);
return result;
};
本期示例