从零打造前沿Web聊天组件:从设计到交互

作者现在制作一款网页端聊天室(青春版),之前一直有这个想法,现在总算是迈出了第一步开始制作了… 雄关漫道真如铁,而今迈步从头越!

启程

当前已经完成左侧聊天室列表显示,通过http://localhost:10086/chatRoom/chatRoomList接口进行获取,可选传入当前用户id,这样将返回所有存在此用户的聊天室数据。

数据从node + express后端获取,存储数据库为mongoDB

  • 获取到的数据:

在这里插入图片描述

  • 当前效果:

在这里插入图片描述


组件设计

在聊天室项目里,右侧的聊天内容区域组件设计要慎重,毕竟属于项目当中的重中之重。

因为聊天一般分为群聊和私聊,所以我准备开发两种复用组件groupChat(群聊)和privateChat(私聊)。

关于组件的引入,有两种方案可选:动态组件模式单一组件条件渲染

这里我选择使用 动态组件模式,其优点:

  1. 类型隔离:群聊/私聊逻辑完全解耦
  2. 性能优化key 保证切换时完全重建实例,可以进行异步加载
  3. 可扩展性:后续想到新的聊天类型,添加起来也非常方便

组件通信规范
- 父组件 → 子组件:Props
- 子组件 → 父组件:Emit
- 跨层级通信:Provide/Inject

这两种聊天组件当中也具有相同的结构,实现私聊和群聊组件的高复用性设计,关键在于将通用逻辑特殊逻辑分离。

在这里插入图片描述

对于设计思路,准备使用组合式API + 插槽组件的方式。

动手:组件创建

创建相应的组件文件,groupChat.vueprivateChat.vueBaseMessage.vue三个组件文件

在这里插入图片描述

BaseMessage.vue中主要由三部分组成,这也是群聊和私聊的共同点,三部分分别是:

  1. 顶部:群聊显示聊天室名称,私聊显示对方用户名
  2. 内容:消息的滚动条列表,用户头像、消息气泡、消息时间,间隔时间较久的消息间显示时间分割线
  3. 底部:消息输入框,可输入文字、图片、表情包

可以先添加相应插槽进入每一块元素,这样可以增强组件的可扩展性。

在这里插入图片描述

接下来在群聊和私聊组件当中引入基础插件,传入聊天室名称

在这里插入图片描述

将群聊组件和私聊组件引入聊天室当中,使用动态切换的方式进行加载。

<div class="chat-main"><!-- 动态组件模式 --><component :is="curChatRoom.key" :key="curChatRoom.key" :chatRoom="curChatRoom" />
</div>
...
<script>import privateChat from "@/components/privateChat.vue";import groupChat from "@/components/groupChat.vue";// 定义组件选项,这样下面可以直接用字符串名称代表组件defineOptions({components:{groupChat,privateChat}})// 定义当前聊天的聊天室,动态组件的key,用于强制组件重新渲染const curChatRoom = reactive({roomId: "",key: "groupChat",roomName: "",roomType: "public"});// 切换聊天室,设置当前聊天室和组件const switchChatRoom = (room) => {curChatRoom.roomId = room.roomId;curChatRoom.key = room.roomType == "private" ? "privateChat" : "groupChat";curChatRoom.roomName = room.roomName;}
</script>

当前效果可做到点击切换左侧聊天室,右侧群聊的名称相应改变

在这里插入图片描述

现在已经生成了可复用的群聊组件。

组件交互

在最外层数据传入群聊groupChat和私聊private组件后,组件都需要根据变化的聊天室roomId去获取此聊天室的聊天记录并展示,因为当前我尚未在后端编写相应接口,先用模拟数据代替。

这里用户头像数据不放在聊天数据一起,这能够减轻聊天数据负担。

// 聊天数据
const mockChatHistory = [{username: '张三',time: new Date('2024-07-01 10:00:00').toLocaleString(),content: '大家好,今天天气不错!'},{username: '李四',time: new Date('2024-07-01 10:05:00').toLocaleString(),content: '是的,适合出去走走。'},{username: '王五',time: new Date('2024-07-01 10:10:00').toLocaleString(),content: '有没有推荐的地方?'}
];// 对应的头像
mockChatUserAvatar = {"张三": 'icon-animal-4',"李四": 'icon-animal-9',"王五": 'icon-animal-1'
}

得到这些数据后,可以将数据传入BaseMessage.vue基础组件当中,当然外部切换聊天室需要内部对roomId进行监听。

	// 假设这里有一个外部动态变化的响应式变量 propswatch(() => props.chatRoom?.roomId, async (newRoomId, oldRoomId) => {if (newRoomId) {await getChatUserAvatar();await getChatHistory();}});

注意:接下来很多内容都是在BaseMessage.vue基础组件中,基础组件由头部、消息列表、输入框三大部分组成

时间分割线

在聊天窗中判断是否添加时间切割线,通常可参考时间维度聊天活跃度维度标准:

  1. 时间维度
  • 日期变化 :当新消息与上一条消息不在同一天时,添加时间切割线,这是最常见的判断方式,能清晰区分不同日期的聊天记录。
  • 设定时间间隔 :根据预设的时间间隔(如每 5 分钟、每小时等)来判断。当两条消息的时间间隔超过设定值,就添加时间切割线,方便用户按特定时间范围查看聊天记录 。
  1. 聊天活跃度维度
  • 消息密集程度 :若一段时间内聊天信息较为密集,即使在同一天,也可能添加多个时间切割线,以区分不同活跃时段的聊天内容;反之,若聊天信息比较稀疏,即使跨越几天,也可能不添加切割线。

根据传入的时间判断时间分割线是否显示:

// 判断时间分隔线是否显示
const shouldShowTimeDivider = (index) => {if (index === 0) return true; // 第一个消息显示分隔线const currentTime = new Date(props.mockChatHistory[index].time);const previousTime = new Date(props.mockChatHistory[index - 1].time);if (currentTime - previousTime > 5 * 60 * 1000) { // 超过5分钟显示分隔线return true;} else {return false;}
}
// 时间分割线时间显示形式
const formatTime = (time) => {const date = new Date(time);const now = new Date();const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());const yesterday = new Date(today);yesterday.setDate(yesterday.getDate() - 1);const dayBeforeYesterday = new Date(today);dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);// 今天:显示小时:分钟if (date >= today) {return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });} // 昨天:显示昨天 小时:分钟else if (date >= yesterday) {return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 前天:显示前天 小时:分钟else if (date >= dayBeforeYesterday) {return `前天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 今年:显示月份-日期else if (date.getFullYear() === now.getFullYear()) {return `${date.getMonth() + 1}${date.getDate()}${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 往年:显示xxxx年else {return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}
}

对于时间分割线上时间内容显示:

  • 当日时间:hh:mm
  • 昨天/前天: 昨天/前天 hh:mm
  • 今年内: 月份日期 hh:mm
  • 以往年份:年份月份日期 hh:mm

在这里插入图片描述

PS:这里对聊天内容界面的元素布局和样式等内容就不详细说明了,都算比较基础的了


消息输入框

聊天消息输入框需要兼顾用户体验、功能完备性和技术实现优雅性,当然现在我只进行基础的设计

需要考虑的内容

  • 输入框高度调节
  • 功能扩展区
  • @成员提及
  • 表情插入

高度调节

给底部chat-footer元素添加伪类,高度3px,设置ns-resize双向调整大小光标

在这里插入图片描述

CSS样式:

&::before {content: '';position: absolute;top: 0px;left: 0;right: 0;height: 3px;background-color: #ccc;cursor: ns-resize;z-index: 1;
}

效果:

在这里插入图片描述

现在光设置css样式还不能拖动边线,需要添加相应的Js方法:(这里在onDragMove移动方法中,设置可移动的最大200px最小100px,chat-content是底部输入框上面的聊天内容区域)

// 底部footer输入框拖动方法
const footerRef = ref(null);
const isDragging = ref(false);
const startY = ref(0);
const startHeight = ref(0);
// 底部输入框顶部边拖动 鼠标按下事件
const onDragStart = (e) => {e.preventDefault(); // 阻止默认行为isDragging.value = true;startY.value = e.clientY;startHeight.value = footerRef.value.offsetHeight;document.addEventListener('mousemove', onDragMove);document.addEventListener('mouseup', onDragEnd);document.body.style.userSelect = 'none'; // 禁用文本选择
};
// 底部输入框顶部边拖动 鼠标移动事件
const onDragMove = (e) => {if (!isDragging.value) return;const dy = e.clientY - startY.value;const newHeight = startHeight.value - dy;if (newHeight >= 100 && newHeight <= 200) {footerRef.value.style.height = `${newHeight}px`;document.querySelector('.chat-content').style.height = `calc(100% - 50px - ${newHeight}px)`;}
};
// 底部输入框顶部边拖动 鼠标松开事件
const onDragEnd = () => {isDragging.value = false;document.removeEventListener('mousemove', onDragMove);document.removeEventListener('mouseup', onDragEnd);document.body.style.userSelect = ''; // 恢复文本选择
};onMounted(() => {footerRef.value = document.querySelector('.chat-footer');// 直接在footer元素上监听mousedown事件,通过event.target判断是否点击了顶部边框footerRef.value.addEventListener('mousedown', (e) => {if (e.offsetY <= 3) { // 判断是否点击了顶部3px区域onDragStart(e);}});
});

在这里插入图片描述

功能扩展区

有最基础的表情和聊天记录icon按钮,如果后续有需要可以继续添加扩展功能,考虑到后续可能群里和私聊功能不同,添加了slot扩展

<!-- 功能列表 -->
<div class="input-function-list"><div class="input-function-item" v-for="funItem in inputFunctionList" :key="funItem.name" :title="funItem.text"><svg class="icon" aria-hidden="true"><use :xlink:href="'#' + funItem.icon"></use></svg></div><slot name="input-extra-function"></slot>
</div>

基础输入框功能:

// 输入框功能列表
const inputFunctionList = [{ name: 'emoji', icon: 'icon-biaoqing', text: '表情', event: () => { console.log('点击了表情') }   },{ name: 'record', icon: 'icon-liaotianjilu', text: '聊天记录', event: () => { console.log('点击了聊天记录')} }
];

可编辑DIV输入框

使用div的contenteditable去制作一个可输入消息框,让div元素可编辑

<div class="input-area"><!-- 可扩展的输入区域 --><slot name="input-area-solt"><div ref="editableDiv" class="editable-area" contenteditable @input="handleInput"@keydown.enter.exact.prevent="sendMessage"@paste="handlePaste"></div></slot>
</div>

这里设计:

  • enter:发送消息
  • shift + enter:换行

通过@keydown.enter.exact.prevent已经让回车键和发送消息方法关联起来了,现在设置shift + enter 配置换行。在div中配置 @keydown.shift.enter.prevent="handleShiftEnter"

handleShiftEnter方法用于获取当前选区,在输入框文本对象末尾添加换行节点br,并重新设置光标位置

// 输入框换行事件处理
const handleShiftEnter = (e) => {e.preventDefault();const selection = window.getSelection();// 检查选区是否在可编辑区域内if (!selection.containsNode(editableDiv.value, true)) {return;}const range = selection.getRangeAt(0);// 创建并插入换行节点const br = document.createElement('br');range.insertNode(br);// 创建新范围并设置光标位置const newRange = document.createRange();newRange.setStartAfter(br);  // 在新创建的 <br> 元素后设置光标位置newRange.collapse(true);// 更新选区selection.removeAllRanges();selection.addRange(newRange);
}

效果:

在这里插入图片描述

当前的复制会将颜色复制过来,所以需要设置handlePaste复制方法,只复制文本内容

在这里插入图片描述

这里对于图片数据复制时,也需要进行在输入框自动缩小图片尺寸,主要修改包括:

  • 检测剪贴板中的图片数据
  • 使用 FileReader 读取原始图片数据(event.target.result)创建 img 元素
  • 保持原有的 maxWidth 样式设置将图片缩小到最大宽度100px
  • 保持图片比例不变
  • 将缩小后的图片插入到可编辑区域
  • 仍然保留原有的纯文本粘贴功能
// 输入框复制事件处理
const handlePaste = (e) => {e.preventDefault();// 检查是否由图片数据const clipboardData = e.clipboardData;if (clipboardData.files && clipboardData.files.length > 0) {const file = clipboardData.files[0];if (file.type.startsWith('image/')) {const reader = new FileReader();reader.onload = (event) => {const range = window.getSelection().getRangeAt(0);range.deleteContents();const imgElement = document.createElement('img');imgElement.src = event.target.result;imgElement.style.maxWidth = '100px';range.insertNode(imgElement);};reader.readAsDataURL(file);return;}}// 处理纯文本粘贴const text = e.clipboardData.getData('text/plain');document.execCommand('insertHTML', false, text);
}

在这里插入图片描述

to be continued

让作者先歇歇吧,成员提及和表情包插入功能尚未开发,后续还要加入最重要WebSocket发送机制

这些都将再下篇文章中出现,先到此这里吧

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络 : 传输层协议UDP与TCP

计算机网络 &#xff1a; 传输层协议UDP与TCP 目录 计算机网络 &#xff1a; 传输层协议UDP与TCP引言1. 传输层协议UDP1.2 UDP协议段格式1.3 UDP的特点1.4 面向数据报1.5 UDP的缓冲区1.6 基于UDP的应用层协议及使用注意事项 2. 传输层协议TCP2.1 再谈端口号2.2 TCP协议段格式2.…

Java高频面试之并发编程-27

hello啊&#xff0c;各位观众姥爷们&#xff01;&#xff01;&#xff01;本baby今天又来报道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面试&#xff1a;详细说说AtomicInteger 的原理 AtomicInteger 的原理详解 AtomicInteger 是 Java 并发包 (java.util.concurrent.atomic)…

冒险岛的魔法果实-多重背包

问题描述 在冒险岛的深处&#xff0c;小萌探索到了一个传说中的魔法果实园。这里满是各种神奇的魔法果实&#xff0c;吃了可以增加不同的魔法能量。 小萌想带一些魔法果实回去&#xff0c;但是他的背包空间有限。看着这些琳琅满目的魔法果实&#xff0c;小萌很是纠结&#xf…

atomicity of memory accesses

文章目录 atomicity of memory accesses✅ 正确认识原子性的边界对于 **Load**&#xff1a;✅ 正确的原子性边界是&#xff1a;对于 **Store**&#xff1a;✅ 正确的原子性边界是&#xff1a; &#x1f504; 修正原文中的说法&#xff08;对照分析&#xff09;✅ 原子性边界最终…

VScode安装配置PYQT6

开始是准备安装PYQT5的&#xff0c;但是安装不下去&#xff0c;就改成安装PYQT6 一.安装pyqt5&#xff0c;成功。 c:\PYQT>pip install pyqt5 Defaulting to user installation because normal site-packages is not writeable Collecting pyqt5 Downloading PyQt5-5.15.…

SpringBoot使用oshi获取服务器相关信息

概念 OSHI是Java的免费基于JNA的&#xff08;本机&#xff09;操作系统和硬件信息库。它不需要安装任何其他本机库&#xff0c;并且旨在提供一种跨平台的实现来检索系统信息&#xff0c;例如操作系统版本&#xff0c;进程&#xff0c;内存和CPU使用率&#xff0c;磁盘和分区&a…

Spring Boot 3 集成 MyBatis 连接 MySQL 数据库

Spring Boot 3 集成 MyBatis 连接 MySQL 数据库的步骤&#xff1a; 以下是集成 Spring Boot 3、MyBatis、HikariCP 连接池并操作 MySQL 数据库的完整步骤和代码&#xff1a; 一、创建 Spring Boot 项目 添加以下依赖&#xff1a; <dependencies><!-- Spring Web --…

基于React + FastAPI + LangChain + 通义千问的智能医疗问答系统

&#x1f4cc; 文章摘要&#xff1a; 本文详细介绍了如何在前端通过 Fetch 实现与 FastAPI 后端的 流式响应通信&#xff0c;并支持图文多模态数据上传。通过构建 multipart/form-data 请求&#xff0c;配合 ReadableStream 实时读取 AI 回复内容&#xff0c;实现类似 ChatGPT…

YOLOv8 升级之路:主干网络嵌入 SCINet,优化黑暗环境目标检测

文章目录 引言1. 低照度图像检测的挑战1.1 低照度环境对目标检测的影响1.2 传统解决方案的局限性2. SCINet网络原理2.1 SCINet核心思想2.2 网络架构3. YOLOv8与SCINet的集成方案3.1 总体架构设计3.2 关键集成代码3.3 训练策略4. 实验结果与分析4.1 实验设置4.2 性能对比4.3 可视…

所有的Linux桌面环境

Linux操作系统提供了多种桌面环境&#xff0c;每种都有其独特的特点和适用场景。以下是一些常见的Linux桌面环境&#xff1a; 轻量级桌面环境 Xfce&#xff1a;广泛使用的轻量级桌面环境&#xff0c;适合资源有限的设备。Xfce 4.18带来了性能改进和新功能&#xff0c;如Thuna…

@component、@bean、@Configuration的区别

详细解析Spring框架中这三个最核心、也最容易混淆的注解&#xff1a;Component、Bean和Configuration。 为了快速理解&#xff0c;我们先看一个总结性的表格&#xff1a; 注解应用级别作用使用场景Component类级别将类标识为Spring组件&#xff0c;让Spring自动扫描并创建实例…

Android多媒体——音/视同步数据处理(二十)

在多媒体播放过程中,音频数据的处理不仅要保证其解码和输出的连续性,还需要与视频帧保持时间上的严格对齐,以实现良好的观看体验。Android 多媒体框架中的 NuPlayerRenderer 是负责最终渲染音视频数据的核心组件之一。 一、Audio数据处理 NuPlayerRenderer 是 Android 原生…

MYSQL 使用命令mysqldump备份数据库的时候需要用户具备什么权限

背景 之前都是使用数据库root用户备份数据库&#xff0c;没有权限问题&#xff0c;今天使用一个数据库基本用户备份数据库&#xff0c;提示一直没有权限&#xff0c;提示的很明显 mysqldump: Error: Access denied; you need (at least one of) the PROCESS privilege(s) for …

WebRTC源码线程-1

1、概述 本篇主要是简单介绍WebRTC中的线程&#xff0c;WebRTC源码对线程做了很多的封装。 1.1 WebRTC中线程的种类 1.1.1 信令线程 用于与应用层的交互&#xff0c;比如创建offer&#xff0c;answer&#xff0c;candidate等绝大多数的操作 1.1.2 工作线程 负责内部的处理逻辑&…

spring:使用标签xml静态工厂方法获取bean

在spring可以直接通过配置文件获取bean对象&#xff0c;如果获取的bean对象还有若干设置&#xff0c;需要自动完成&#xff0c;可以通过工厂方法获取bean对象。 静态工厂类&#xff0c;其中InterfaceUserDao和InterfaceUserService都是自定义的接口&#xff0c;可以自己替换。…

linux 用户态时间性能优化工具perf/strace/gdb/varlind/gprof

1. perf top -g或者top分析卡顿(cpu占用比较高的函数) gdb 是 GNU 调试器,可以用于分析程序的时间性能。虽然 info time 不是直接用于性能分析的命令,但 gdb 提供了与时间相关的功能,例如通过 timer 命令设置计时器或通过 info proc 查看进程的时间信息。 #include <…

客户端和服务器已成功建立 TCP 连接【输出解析】

文章目录 图片**1. 连接状态解析****第一条记录&#xff08;服务器监听&#xff09;****第二条记录&#xff08;客户端 → 服务器&#xff09;****第三条记录&#xff08;服务器 → 客户端&#xff09;** **2. 关键概念澄清****(1) 0.0.0.0 的含义****(2) 端口号的分配规则** *…

Win系统下的Linux系统——WSL 使用手册

我们在复现一些项目的时候&#xff0c;有些依赖包只能在 linux 环境下使用&#xff0c;还不打算使用远程服务器&#xff0c;那么此时我们可以使用 WSL 创建一个 ubutu 系统&#xff0c;在这个系统里创建虚拟环境、下载依赖包。然后&#xff0c;我们就可以在 windows 下的 vscod…

电脑同时连接内网和外网的方法,附外网连接局域网的操作设置

对于工作一般都设置在内网网段中&#xff0c;而同时由于需求需要连接外网&#xff0c;一般只能通过内网和外网的不断切换进行设置&#xff0c;如果可以同时连接内网和外网会更加便利&#xff0c;同时连接内网和外网方法具体如下。 一、电脑怎么弄可以同时连接内网和外网&#…

C++11:原子操作与内存顺序:从理论到实践的无锁并发实现

文章目录 0.简介1.并发编程需要保证的特性2.原子操作2.1 原子操作的特性 3.内存顺序3.1 顺序一致性3.2 释放-获取&#xff08;Release-Acquire)3.3 宽松顺序&#xff08;Relaxed)3.4 内存顺序 4.无锁并发5. 使用建议 0.简介 在并发编程中&#xff0c;原子性、可见性和有序性是…