阅读量:0
以上篇文章的代码为基准,继续扩展实现艾特@功能,没看过的可以点击链接查看前端实现输入框表情输入及发送,仿b站、qq小表情输入
此demo使用vue3
实现效果图
声明:这只是个demo,不涉及与后端交互,不过会在该交互的地方标记,如需实际应用于项目,请根据实际情况进行改造完善!
艾特列表组件定义
<template> <!-- userGroupPosition父组件传递显示定位 --> <div class="user-group" :style="{ left: userGroupPosition.x, top: userGroupPosition.y }"> <ul class="user-list"> <li v-for="(user, index) in userGroup" :key="user.id" class="user-item" :class="{ active: activeIndex === index }" @click="selectAtUser(user)"> <img :src="user.avatar" /> <span>{{ user.name }}</span> </li> </ul> </div> </template> <script setup> import { ref, defineProps, toRefs, onMounted, defineEmits } from "vue"; /** * @param userGroup 聊天群员列表 * @param userGroupPosition 列表组件显示定位 */ const props = defineProps(["userGroup", "userGroupPosition"]); // 选择群员事件 const emit = defineEmits(["atUser"]); const { userGroup, userGroupPosition } = toRefs(props); // 此变量用来使用键盘上下按钮时,选择群员 let activeIndex = ref(-1); // 给window挂载键盘事件,这样使用键盘上下键时可以选择群员 onMounted(() => { window.addEventListener("keydown", keyboardSelect); }); // 键盘事件 通过修改activeIndex值,达到选择群员 function keyboardSelect(e) { if (e.key === "ArrowDown") { if (activeIndex.value !== userGroup.value.length - 1) { activeIndex.value++; } else { activeIndex.value = 0; } } else if (e.key === "ArrowUp") { if (activeIndex.value <= 0) { activeIndex.value = userGroup.value.length - 1; } else { activeIndex.value--; } } else if (e.key === "Enter") { selectAtUser(userGroup.value[activeIndex.value]); } } // 给父组件传递选择的群员 function selectAtUser(user) { emit("atUser", user); } </script> <style scoped> .user-group { position: fixed; } .user-list { list-style: none; padding: 0; border: 1px solid #e1e1e1; border-radius: 6px; } .user-item { height: 50px; line-height: 50px; padding: 0 20px; cursor: pointer; background-color: white; &:hover, &.active { background-color: #e1e1e1; } &:not(:last-of-type) { border-bottom: 1px solid #e1e1e1; } img { width: 30px; vertical-align: middle; } } </style>
父组件主逻辑处理
首先修改消息输入框,增加输入事件,用以判断显示输入框。且在上篇中发送消息采用了@keydown.enter.prevent绑定事件的形式发送,直接改成@keydown,用判断的方式发送,因为显示艾特输入框时,按回车是要选择群员的
<!-- 消息输入框 --> <div class="msg-input" ref="msgInput" contenteditable @blur="getAfterBlurIndex" @keydown="msgInputKeyDown" @input="msgInputFun"></div> <!-- 可艾特的聊天群员 --> <userGroup :userGroup="userGroupData" :userGroupPosition="userGroupPosition" v-if="showAtSelect" @at-user="selectAtUser"></userGroup>
定义相关变量,群员列表、列表组件定位坐标、是否显示艾特组件
// 群员列表 const userGroupData = reactive([ { name: "用户A", id: 1, avatar: require("@/assets/logo.png") }, { name: "用户B", id: 2, avatar: require("@/assets/logo.png") }, { name: "用户C", id: 3, avatar: require("@/assets/logo.png") }, ]); // 用户列表定位 const userGroupPosition = reactive({ x: "0px", y: "0px", }); let showAtSelect = ref(false);
定义输入事件,当输入@时,获取输入框光标相对页面坐标,并显示输入框组件。(失焦时,在失焦事件getAfterBlurIndex里,使用 setTimeout(() => {
showAtSelect.value = false;}, 100);隐藏艾特组件)
// 输入框输入事件 function msgInputFun(e) { if (e.data === "@") { setTimeout(() => { const { x, y } = getCursorPosition(); showAtSelect.value = true; userGroupPosition.x = x + "px"; userGroupPosition.y = y + "px"; }, 100); } else { showAtSelect.value = false; } } // 获取输入框中的光标相对页面的坐标 function getCursorPosition() { let sel = window.getSelection(); let range = document.createRange(); range.selectNode(sel.focusNode); range.setStart(sel.focusNode, sel.focusOffset); const { x, y } = range.getBoundingClientRect(); return { x, y }; }
这个时候可以显示出来群员选择列表了,再来定义键盘事件,在列表组件显示时阻止上下按钮的默认行为。如果不显示且按下回车时发送消息
// 显示艾特选择组件时,阻止输入框默认上下按键及回车行为 function msgInputKeyDown(e) { if (showAtSelect.value && ["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) { return e.preventDefault(); } if (e.key === "Enter") { e.preventDefault(); sendMsg(); } }
然后编写选择群员事件,先获取并设置输入框选区,创建一个span标签节点,把被艾特人相关信息填入后插入到输入框子节点里。注意:创建的span标签里写了一个@,所以需要chatInputOffset.deleteContents()删除原@字符
function selectAtUser(user) { showAtSelect.value = false; msgInput.value.blur(); getInputSelection(); chatInputOffset.setStart(focusNode, focusOffset.value - 1); chatInputOffset.setEnd(focusNode, focusOffset.value); chatInputOffset.deleteContents(); const atElement = `<span userId='${user.id}' userName='${user.name}' contentEditable="false" class="at-msg" style="color:#2E77E5">@${user.name} </span>`; chatInputOffset.collapse(false); const node = chatInputOffset.createContextualFragment(atElement); let c = node.lastChild; chatInputOffset.insertNode(node); if (c) { chatInputOffset.setEndAfter(c); chatInputOffset.setStartAfter(c); } let j = window.getSelection(); j.removeAllRanges(); j.addRange(chatInputOffset); }
这个时候就可以把@消息发送出去了
let msgType = ref("text"); // 发送的消息类型 function sendMsg() { console.log("msgInput:", msgInput); chatMsgRecord.value = ""; // 先清空一下旧消息 msgType.value = "text"; // 初始化消息类型 msgInput.value.childNodes.forEach((element) => { // 如果是emoji表情图片的话,则转义 if (element.nodeName === "IMG" && element.className === "emoji") { chatMsgRecord.value += element.alt; } else if (element.className === "at-msg") { chatMsgRecord.value += element.innerText + " "; msgType.value = "groupChatAt"; // 根据实际约束类型修改 } else { chatMsgRecord.value += element.data; } }); // 清空输入框中的内容 msgInput.value.innerHTML = ""; msgInput.value.innerText = ""; // 在这里使用websocket把数据发送给后端 // socketSend({text:chatMsgRecord,type:msgType}) }
如有更好的意见欢迎在评论区指出!