express+vue在线im实现【二】

express+vue在线im实现【一】

在线体验

本期完成了:
1、心跳检测
2、支持发送表情与图片【这个目前还需要优化下,当图片上传后会被默认选中,需要点击一下旁边,使之失去选中效果,才能正常,留待下期优化吧】
3、新增了一些细节,消息固定位置,是否显示呢称,新消息来的闪烁提示等
4、将整个模块独立了出来,在博客页新增了全局挂载
5、如何处理图片的加载无法正常获取到准确高度,导致无法滚动到准确位置(这个是这期本人觉得最复杂的,等待图片加载完再获取高度,体验太差;设置固定高度,又无法兼容到小图片,大的图片有看不清;具体解决方案在下方)

下期功能

1、文件发送
2、在线语音

感兴趣的,可以私聊我,也可以点个收藏,关注,以下是核心代码示例

心跳检测

为何需要做这个,长连接不稳定,会自动断开,需要我们手动来做在线检测和重连

        // 轮询心跳检测
        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属性来完成

    <div
                :id="myInputId"
                class="im-input kl-contenteditable-input flex-1 no-select f-14"
                contenteditable="true"
                @paste="pasteEvent($event)"
                @blur="blurEvent"
            ></div>
  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)
                }
            }
        },

如何解决图片高度问题

前端部分

上传:可以看到我们在上传图片时同时上传了图片的高度与宽度

  // 上传图片,同时需要上传图片的宽高
                            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('请重新上传')
                        }

回显:直接读取链接上的宽高,来计算出需要呈现的最终宽高,这样就可以不用等到图片加载完毕,就能自动滚动到准确位置

    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的上传代码

这边我们需要接收宽高,并将宽高信息放到文件名上

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;
};

本期示例

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/715530.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Quartus Prime18.1的安装与FPGA的基础仿真教程

Quartus是一种美国科技公司Intel&#xff08;英特尔&#xff09;公司开发的FPGA&#xff08;现场可编辑门阵列&#xff09;设计编译软件&#xff0c;用作设计、仿真、综合和布局、支持多种编程语言&#xff0c;包括VHDL、Verilog等&#xff0c;并具有丰富的功能和工具库&#x…

17.Meta AI 大模型家族 LLaMA

Meta LLaMA 1 大模型技术解读 LLaMA 1&#xff1a;小模型大数据 LLaMA 1 在万亿 Token 公开数据集上预训练 LLaMA 1 模型网络架构改进 大模型网络架构差异性配置总览 典型大模型网络架构对比 LLaMA 1 预训练超参数配置 典型大模型训练超参数对比 LLaMA 1 预训练效率提升与成本…

net start mysql服务名无效

问题背景 起因是我的电脑因为停电烧坏了系统固态硬盘&#xff0c;再新装系统后&#xff0c;之前的MySQL服务无法通过下面的命令启动。 net start mysql # 报错&#xff1a;服务名无效 报错&#xff1a;服务名无效 报错信息 未找到&#xff1a;在Windows服务中找不到MySQL 找…

增强大型语言模型(LLM)可访问性:深入探究在单块AMD GPU上通过QLoRA微调Llama 2的过程

Enhancing LLM Accessibility: A Deep Dive into QLoRA Through Fine-tuning Llama 2 on a single AMD GPU — ROCm Blogs 基于之前的博客《使用LoRA微调Llama 2》的内容&#xff0c;我们深入研究了一种称为量化低秩调整&#xff08;QLoRA&#xff09;的参数高效微调&#xff0…

【APP_汽修宝】数据采集案例APP_数据解密分析

如果不会写代码&#xff0c;那就出书、写博客、做视频、录播客。 &#x1f4da; S35赛季末王者昭君罗 关键代码定位 使用方法【逆向-快速定位关键代码】通过hook常用函数HashMap方法 动态分析 下面是我们通过访问目标页面时 Frida hook 捕获HashMap的调…

Nginx与Gateway

Nginx与Gateway Nginx 基本介绍 Nginx 是一款轻量级的高性能 Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。它由俄罗斯的 Igor Sysoev 所开发&#xff0c;最初供俄罗斯大型的门户网站及搜索引擎 Rambler 使用。 Nginx 的特点在于其占用…

RIP解决不连续子网问题

#交换设备 RIP解决不连续子网问题 一、不连续子网的概念 相同主网下的子网&#xff0c;被另一个主网分割&#xff0c;例如下面实验拓扑在某公司的网络整改项目中&#xff0c;原先R1 和RS 属于同一主网络 10.0.0.0/8&#xff0c;现被 R2、R3、R4 分离&#xff0c;整网采用了 …

从xxl-job源码中学习Netty的使用

1. 启动与Spring实例化 com.xxl.job.core.executor.impl.XxlJobSpringExecutor.java类 继承SmartInitializingSingleton 类&#xff0c;在afterSingletonsInstantiated 实例化后方法中 调用initJobHandlerMethodRepository 把所有的xxljob任务管理起来&#xff1b; private…

.net8系列-图文并茂手把手教你使用Nlog记录.net日志

Nlog是什么&#xff1f; NLog是一个为.NET平台设计的灵活且免费的日志记录库。它适用于包括.NET Framework、.NET Core和.NET Standard在内的多种.NET环境。 Nlog有什么好处或者特点&#xff1f; 配置灵活&#xff1a;NLog允许开发者通过配置文件&#xff08;通常是NLog.conf…

串口触摸屏分割字符串

分割字符串的方法1、split2、indexOf()3、substr(start,length) 分割字符串的方法 1、split&#xff1a;将一个字符串分割为子字符串&#xff0c;然后将结果作为字符串数组返回。 2、indexOf() &#xff1a;返回某个指定的字符串值在字符串中首次出现的位置&#xff08;从左向右…

机器人、人工智能相关领域 news/events (专栏目录)

Some Insights 一些机器人、人工智能或相关领域的news、events等 1. 智能制造 - 你需要了解的 10 个基本要素 2. 现实世界中的人工智能&#xff1a;工业制造的 4 个成功案例研究 3. 企业使用人工智能情况调查 4. 未来工厂中的人工智能&#xff1a;人工智能加速制造成果规模…

用React编写一个密码组件表单

theme: condensed-night-purple highlight: atelier-cave-light 背景介绍 我们在使用网站或者应用程序的登录界面或创建帐户界面时&#xff0c;往往避免不了需要用户输入密码这一步骤&#xff0c;而用户是否可以选择看见他们输入的密码是十分重要的一项功能。尤其是在当输入的…

QT绘图项目 - 汽车表盘

目录 前言: 整体代码 widget.h widget.cpp 效果演示 实现刻度文字正确排版 优化代码 达到效果 封装整理代码结构: widget.h widget.cpp 指针样式美化 优化后的指针API 效果演示 设置高速刻度为红色 优化刻度API 效果演示 速度显示优化 给内圈画上黑色 优化速度…

【学习-华为HCIA数通网络工程师真题(持续更新)】(2024-6-16更)

1、在 VRP 平台上&#xff0c;可以通过下面哪种方式访向上条历史命令&#xff1f; 上光标 &#xff08;ctrlU 为自定义快捷键&#xff0c;ctrlP 为显示历史缓存区的前一条命令&#xff0c;左光标为移动光标&#xff09; 2、主机 A &#xff08;10.1.1.2/24&#xff09;和主机 B…

在亚马逊云上部署WordPress:一个完整的LAMP环境安装教程

什么是LAMP LAMP是一个流行的开源软件堆栈&#xff0c;用于网站和网络应用程序的开发和部署。LAMP是几个主要组件的首字母缩写&#xff0c;包括&#xff1a; Linux&#xff1a;操作系统层&#xff0c;LAMP通常部署在Linux操作系统上&#xff0c;但它也可以使用其他类似Unix的…

Golang | Leetcode Golang题解之第155题最小栈

题目&#xff1a; 题解&#xff1a; type MinStack struct {stack []intminStack []int }func Constructor() MinStack {return MinStack{stack: []int{},minStack: []int{math.MaxInt64},} }func (this *MinStack) Push(x int) {this.stack append(this.stack, x)top : thi…

牛客周赛 Round 47 解题报告 | 珂学家

前言 题解 这真的是牛客周赛&#xff1f; 哭了 欢迎关注 珂朵莉 牛客周赛专栏 珂朵莉 牛客小白月赛专栏 A. 小红的葫芦 签到题 但是写起来有点变扭&#xff0c;方法应该蛮多的 统计分组 有2组一组长度为2&#xff0c;一组长度为3 def check(arr):arr.sort()if arr[0] …

Git学习2 -- VSCode中的Git

看了下&#xff0c;主要的插件有3个。自带的Source Control。第1个是Gitlens&#xff0c;第2个是Git Graph。第三个还有个git history。 首先是Source Control。界面大概是这样的。 还是挺直观的。在第一栏source control&#xff0c;可以进行基本的git操作。主要的git操作都是…

MongoDB~高可用集群介绍:复制集群(副本集)、分片集群

背景 MongoDB 的集群主要包括副本集&#xff08;Replica Set&#xff09;和分片集群&#xff08;Sharded Cluster&#xff09;两种类型。 副本集 组成&#xff1a;通常由一个主节点&#xff08;Primary&#xff09;和多个从节点&#xff08;Secondary&#xff09;构成。 功…

Objective-C 学习笔记 | init

Objective-C 学习笔记 | init Objective-C 学习笔记 | init编写 init 方法禁用 init 方法 Objective-C 学习笔记 | init init 是实例方法&#xff0c;负责初始化对象&#xff0c;并返回初始化后的对象的地址。 编写 init 方法 完整代码见于&#xff1a;UestcXiye/Objective-…