青白江区城乡和建设局网站,网站流量一直做不起来,0453牡丹江信息网手机版,整站优化系统作者现在制作一款网页端聊天室#xff08;青春版#xff09;#xff0c;之前一直有这个想法#xff0c;现在总算是迈出了第一步开始制作了… 雄关漫道真如铁#xff0c;而今迈步从头越#xff01;
启程
当前已经完成左侧聊天室列表显示#xff0c;通过http://localhos…作者现在制作一款网页端聊天室青春版之前一直有这个想法现在总算是迈出了第一步开始制作了… 雄关漫道真如铁而今迈步从头越
启程
当前已经完成左侧聊天室列表显示通过http://localhost:10086/chatRoom/chatRoomList接口进行获取可选传入当前用户id这样将返回所有存在此用户的聊天室数据。
数据从node express后端获取存储数据库为mongoDB。
获取到的数据 当前效果 组件设计
在聊天室项目里右侧的聊天内容区域组件设计要慎重毕竟属于项目当中的重中之重。
因为聊天一般分为群聊和私聊所以我准备开发两种复用组件groupChat群聊和privateChat私聊。
关于组件的引入有两种方案可选动态组件模式 和 单一组件条件渲染
这里我选择使用 动态组件模式其优点
类型隔离群聊/私聊逻辑完全解耦性能优化key 保证切换时完全重建实例可以进行异步加载可扩展性后续想到新的聊天类型添加起来也非常方便
组件通信规范 - 父组件 → 子组件Props - 子组件 → 父组件Emit - 跨层级通信Provide/Inject
这两种聊天组件当中也具有相同的结构实现私聊和群聊组件的高复用性设计关键在于将通用逻辑与特殊逻辑分离。 对于设计思路准备使用组合式API 插槽组件的方式。
动手组件创建
创建相应的组件文件groupChat.vue、privateChat.vue和BaseMessage.vue三个组件文件 在BaseMessage.vue中主要由三部分组成这也是群聊和私聊的共同点三部分分别是
顶部群聊显示聊天室名称私聊显示对方用户名内容消息的滚动条列表用户头像、消息气泡、消息时间间隔时间较久的消息间显示时间分割线底部消息输入框可输入文字、图片、表情包
可以先添加相应插槽进入每一块元素这样可以增强组件的可扩展性。 接下来在群聊和私聊组件当中引入基础插件传入聊天室名称 将群聊组件和私聊组件引入聊天室当中使用动态切换的方式进行加载。
div classchat-main!-- 动态组件模式 --component :iscurChatRoom.key :keycurChatRoom.key :chatRoomcurChatRoom /
/div
...
scriptimport 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基础组件中基础组件由头部、消息列表、输入框三大部分组成 时间分割线
在聊天窗中判断是否添加时间切割线通常可参考时间维度和聊天活跃度维度标准
时间维度
日期变化 当新消息与上一条消息不在同一天时添加时间切割线这是最常见的判断方式能清晰区分不同日期的聊天记录。设定时间间隔 根据预设的时间间隔如每 5 分钟、每小时等来判断。当两条消息的时间间隔超过设定值就添加时间切割线方便用户按特定时间范围查看聊天记录 。
聊天活跃度维度
消息密集程度 若一段时间内聊天信息较为密集即使在同一天也可能添加多个时间切割线以区分不同活跃时段的聊天内容反之若聊天信息比较稀疏即使跨越几天也可能不添加切割线。
根据传入的时间判断时间分割线是否显示
// 判断时间分隔线是否显示
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最小100pxchat-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 classinput-function-listdiv classinput-function-item v-forfunItem in inputFunctionList :keyfunItem.name :titlefunItem.textsvg classicon aria-hiddentrueuse :xlink:href# funItem.icon/use/svg/divslot nameinput-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 classinput-area!-- 可扩展的输入区域 --slot nameinput-area-soltdiv refeditableDiv classeditable-area contenteditable inputhandleInputkeydown.enter.exact.preventsendMessagepastehandlePaste/div/slot
/div这里设计
enter发送消息shift enter换行
通过keydown.enter.exact.prevent已经让回车键和发送消息方法关联起来了现在设置shift enter 配置换行。在div中配置 keydown.shift.enter.preventhandleShiftEnter
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发送机制
这些都将再下篇文章中出现先到此这里吧