From b905db0cfabb655eef4e0cff9f8144b37007867e Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:29:24 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=92=A4=E5=9B=9E=E9=80=BB=E8=BE=91=E5=92=8C=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=86=85=E5=AE=B9=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整消息菜单的撤回选项显示逻辑,区分单聊和群聊场景 - 修复编辑器内容处理,使用trimEnd替代trim避免尾部空格问题 - 移除重复的quote元素删除操作 - 优化编辑器空内容判断逻辑 --- src/components/editor/CustomEditor.vue | 14 ++++++++------ src/views/message/inner/panel/menu.ts | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index 0ec7906..f68fd81 100644 --- a/src/components/editor/CustomEditor.vue +++ b/src/components/editor/CustomEditor.vue @@ -100,6 +100,7 @@ const handleInput = (event) => { const editorClone = target.cloneNode(true) const quoteElements = editorClone.querySelectorAll('.editor-quote') quoteElements.forEach(quote => quote.remove()) + quoteElements.forEach(quote => quote.remove()) // 处理表情图片,将其 alt 属性(表情文本)添加到文本内容中 const emojiImages = editorClone.querySelectorAll('img.editor-emoji') @@ -119,7 +120,8 @@ const handleInput = (event) => { editorContent.value = textContent // 检查是否需要清空编辑器以显示placeholder - const isEmpty = textContent.trim() === '' && + // 只有当编辑器中没有任何内容(包括空格)且没有其他元素时才清空 + const isEmpty = textContent === '' && !target.querySelector('img, .editor-file, .mention') if (isEmpty && target.innerHTML !== '') { @@ -525,14 +527,14 @@ const sendMessage = () => { if (messageData.items.length === 0 || (messageData.items.length === 1 && messageData.items[0].type === 1 && - !messageData.items[0].content.trim())) { + !messageData.items[0].content.trimEnd())) { return // 没有内容,不发送 } // 处理不同类型的消息 messageData.items.forEach(item => { // 处理文本内容 - if (item.type === 1 && item.content.trim()) { + if (item.type === 1 && item.content.trimEnd()) { const data = { items: [{ content: item.content, @@ -634,7 +636,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) textContent = '' } @@ -674,7 +676,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) textContent = '' } @@ -696,7 +698,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) } diff --git a/src/views/message/inner/panel/menu.ts b/src/views/message/inner/panel/menu.ts index 28c171c..b1b8a11 100644 --- a/src/views/message/inner/panel/menu.ts +++ b/src/views/message/inner/panel/menu.ts @@ -48,9 +48,20 @@ export function useMenu() { dropdown.options.push({ label: '多选', key: 'multiSelect' }) dropdown.options.push({ label: '引用', key: 'quote' }) - if (isRevoke(uid, item)|| (dialogueStore.groupInfo as any).is_manager) { - dropdown.options.push({ label: `撤回`, key: 'revoke' }) + //如果是单聊 + if(item.talk_type===1){ + //撤回时间限制内,并且是自己发的 + if(isRevoke(uid, item)&&item.float==='right'){ + dropdown.options.push({ label: `撤回`, key: 'revoke' }) + } + //群聊 + }else if(item.talk_type===2){ + //管理员可以强制撤回所有成员信息 + if ((dialogueStore.groupInfo as any).is_manager) { + dropdown.options.push({ label: `撤回`, key: 'revoke' }) + } } + dropdown.options.push({ label: '删除', key: 'delete' }) From bdf07155c84fc789330a808bf2024c0c91fad5f9 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:48:52 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D=E6=8F=90?= =?UTF-8?q?=E5=8F=8A=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=94=A8=E6=88=B7ID?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复提及成员时用户ID类型转换问题,确保ID统一为字符串类型。同时为管理员添加"全体成员"提及选项,并完善提及列表的数据处理逻辑。 --- src/components/editor/CustomEditor.vue | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index f68fd81..9fbab75 100644 --- a/src/components/editor/CustomEditor.vue +++ b/src/components/editor/CustomEditor.vue @@ -76,7 +76,9 @@ const navs = ref([ const mentionList = ref([]) const currentMentionQuery = ref('') - +setTimeout(() => { + console.log('props.members',props.members) +}, 1000) // 编辑器内容 const editorContent = ref('') const editorHtml = ref('') @@ -165,7 +167,9 @@ const showMentionList = () => { mentionList.value = props.members.filter(member => { return member.value.toLowerCase().startsWith(query) }) - + if(dialogueStore.groupInfo.is_manager){ + mentionList.value.unshift({ id: 0, nickname: '全体成员', avatar: defAvatar, value: '全体成员' }) +} showMention.value = mentionList.value.length > 0 selectedMentionIndex.value = 0 } @@ -190,6 +194,7 @@ const updateMentionPosition = (range) => { // 插入mention const insertMention = (member) => { + console.log('插入mention',member) const selection = window.getSelection() if (!selection.rangeCount) return @@ -204,7 +209,7 @@ const insertMention = (member) => { // 创建mention元素 const mentionSpan = document.createElement('span') mentionSpan.className = 'mention' - mentionSpan.setAttribute('data-user-id', member.id || member.user_id) + mentionSpan.setAttribute('data-user-id',String(member.id)) mentionSpan.textContent = `@${member.value || member.nickname}` mentionSpan.contentEditable = 'false' @@ -541,10 +546,15 @@ const sendMessage = () => { type: 1 }], mentionUids: messageData.mentionUids, - mentions: [], + mentions: messageData.mentionUids.map(uid => { + return { + atid: uid, + name: mentionList.value.find(member => member.id === uid)?.nickname || '' + } + }), quoteId: messageData.quoteId, } - + console.log('data',data) emit( 'editor-event', emitCall('text_event', data) @@ -604,7 +614,7 @@ const parseEditorContent = () => { // 处理@提及 const userId = node.getAttribute('data-user-id') if (userId) { - mentionUids.push(parseInt(userId)) + mentionUids.push(Number(userId)) } textContent += node.textContent } else if (node.tagName === 'IMG') { From d4e52152efb509bfd4847dd3cdb5c60b1a6bfd92 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:45:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0=E9=BC=A0?= =?UTF-8?q?=E6=A0=87=E7=82=B9=E5=87=BB=E9=80=89=E6=8B=A9mention=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8F=92=E5=85=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增handleMentionSelectByMouse函数处理鼠标点击选择mention - 重构insertMention函数,支持传入range参数并优化插入逻辑 - 修复mention列表点击事件,防止默认行为导致的问题 - 优化onSubscribeMention函数,确保焦点和选区正确处理 --- src/components/editor/CustomEditor.vue | 228 ++++++++++++++++--------- 1 file changed, 146 insertions(+), 82 deletions(-) diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index 9fbab75..d32f597 100644 --- a/src/components/editor/CustomEditor.vue +++ b/src/components/editor/CustomEditor.vue @@ -174,6 +174,23 @@ const showMentionList = () => { selectedMentionIndex.value = 0 } +// 处理鼠标点击选择mention +const handleMentionSelectByMouse = (member) => { + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + insertMention(member, selection.getRangeAt(0).cloneRange()); + } else { + // 如果没有有效的选区,尝试聚焦编辑器并获取选区 + editorRef.value?.focus(); + nextTick(() => { + const newSelection = window.getSelection(); + if (newSelection && newSelection.rangeCount > 0) { + insertMention(member, newSelection.getRangeAt(0).cloneRange()); + } + }); + } +}; + // 隐藏mention列表 const hideMentionList = () => { showMention.value = false @@ -193,66 +210,74 @@ const updateMentionPosition = (range) => { } // 插入mention -const insertMention = (member) => { - console.log('插入mention',member) - const selection = window.getSelection() - if (!selection.rangeCount) return - - const range = selection.getRangeAt(0) - const textNode = range.startContainer - const offset = range.startOffset - - // 找到@符号的位置 - const textContent = textNode.textContent || '' - const atIndex = textContent.lastIndexOf('@', offset - 1) - - // 创建mention元素 - const mentionSpan = document.createElement('span') - mentionSpan.className = 'mention' - mentionSpan.setAttribute('data-user-id',String(member.id)) - mentionSpan.textContent = `@${member.value || member.nickname}` - mentionSpan.contentEditable = 'false' - - if (atIndex !== -1) { - // 如果找到@符号,替换文本 - const beforeText = textContent.substring(0, atIndex) - const afterText = textContent.substring(offset) - - // 创建新的文本节点 - const beforeNode = document.createTextNode(beforeText) - const afterNode = document.createTextNode(' ' + afterText) - - // 替换内容 - const parent = textNode.parentNode - parent.insertBefore(beforeNode, textNode) - parent.insertBefore(mentionSpan, textNode) - parent.insertBefore(afterNode, textNode) - parent.removeChild(textNode) +const insertMention = (member, clonedRange) => { + console.log('插入mention', member); + const selection = window.getSelection(); + if (!clonedRange || !selection) return; + + const range = clonedRange; // 使用传入的克隆 range + + const textNode = range.startContainer; + const offset = range.startOffset; + const textContent = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent || '' : ''; + // @符号的查找逻辑仅当光标在文本节点内且不在开头时才有意义 + const atIndex = (textNode.nodeType === Node.TEXT_NODE && offset > 0) ? textContent.lastIndexOf('@', offset - 1) : -1; + + const mentionSpan = document.createElement('span'); + mentionSpan.className = 'mention'; + mentionSpan.setAttribute('data-user-id', String(member.id)); + mentionSpan.textContent = `@${member.value || member.nickname}`; + mentionSpan.contentEditable = 'false'; + + if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) { + const parent = textNode.parentNode; + if (!parent) return; // Sanity check + + const textBeforeAt = textContent.substring(0, atIndex); + const textAfterCursor = textContent.substring(offset); + + const beforeNode = document.createTextNode(textBeforeAt); + const spaceAfterMentionNode = document.createTextNode(' '); // Ensure space after mention + const afterNode = document.createTextNode(textAfterCursor); + + parent.insertBefore(beforeNode, textNode); + parent.insertBefore(mentionSpan, textNode); + parent.insertBefore(spaceAfterMentionNode, textNode); + parent.insertBefore(afterNode, textNode); + parent.removeChild(textNode); + + range.setStartAfter(spaceAfterMentionNode); + range.collapse(true); } else { - // 如果没有找到@符号,直接在光标位置插入 - range.deleteContents() - - // 插入@提及元素 - range.insertNode(mentionSpan) - - // 在@提及元素后添加空格 - const spaceNode = document.createTextNode(' ') - range.setStartAfter(mentionSpan) - range.insertNode(spaceNode) - - // 将光标移动到空格后 - range.setStartAfter(spaceNode) - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) + // 如果没有找到@符号,或者光标不在合适的文本节点内,直接在当前光标位置插入 + if (!range.collapsed) { + range.deleteContents(); + } + + range.insertNode(mentionSpan); + + const spaceNode = document.createTextNode(' '); + // 正确地将 range 移动到 mentionSpan 之后再插入空格 + const tempRangeForSpace = range.cloneRange(); // 使用临时 range 避免干扰主 range + tempRangeForSpace.setStartAfter(mentionSpan); + tempRangeForSpace.collapse(true); + tempRangeForSpace.insertNode(spaceNode); + + // 将主 range 移动到新插入的空格之后 + range.setStartAfter(spaceNode); + range.collapse(true); } - - // 触发输入事件以更新编辑器内容 - handleInput({ target: editorRef.value }) - - // 隐藏mention列表 - hideMentionList() -} + + selection.removeAllRanges(); + selection.addRange(range); + + editorRef.value?.focus(); // 确保编辑器在操作后仍有焦点 + + nextTick(() => { + handleInput({ target: editorRef.value }); + hideMentionList(); + }); +}; // 处理粘贴事件 const handlePaste = (event) => { @@ -350,12 +375,15 @@ const handleKeydown = (event) => { break case 'Enter': case 'Tab': - event.preventDefault() - const selectedMember = mentionList.value[selectedMentionIndex.value] + event.preventDefault(); + const selectedMember = mentionList.value[selectedMentionIndex.value]; if (selectedMember) { - insertMention(selectedMember) + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + insertMention(selectedMember, selection.getRangeAt(0).cloneRange()); + } } - break + break; case 'Escape': hideMentionList() break @@ -398,6 +426,25 @@ const handleKeydown = (event) => { } prevSibling = prevSibling.previousSibling } + } else { + // 如果光标在文本节点中间或末尾,且当前文本节点只包含空格 + // 检查前一个兄弟节点是否是mention + if (!container.textContent.trim()) { + let prevSibling = container.previousSibling + while (prevSibling) { + if (prevSibling.nodeType === Node.ELEMENT_NODE && + prevSibling.classList && + prevSibling.classList.contains('mention')) { + targetMention = prevSibling + break + } + // 如果是文本节点且不为空,停止查找 + if (prevSibling.nodeType === Node.TEXT_NODE && prevSibling.textContent.trim()) { + break + } + prevSibling = prevSibling.previousSibling + } + } } } else if (container.nodeType === Node.ELEMENT_NODE) { // 如果光标在元素节点中,检查前一个子节点 @@ -930,27 +977,44 @@ const insertImageEmoji = (imgSrc, altText) => { } // 事件监听 -const onSubscribeMention = (data) => { - // 确保编辑器获得焦点 - editorRef.value?.focus() - - // 如果编辑器为空或者光标不在编辑器内,将光标移动到编辑器末尾 - const selection = window.getSelection() - if (!selection.rangeCount || !editorRef.value.contains(selection.anchorNode)) { - const range = document.createRange() - if (editorRef.value.lastChild) { - range.setStartAfter(editorRef.value.lastChild) +const onSubscribeMention = async (data) => { + const editorNode = editorRef.value; + if (!editorNode) return; + + editorNode.focus(); + await nextTick(); // 确保焦点已设置 + + let selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + const range = document.createRange(); + if (editorNode.lastChild) { + range.setStartAfter(editorNode.lastChild); } else { - range.setStart(editorRef.value, 0) + range.setStart(editorNode, 0); } - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) + range.collapse(true); + if (selection) selection.removeAllRanges(); + selection?.addRange(range); + await nextTick(); + selection = window.getSelection(); + } else if (!editorNode.contains(selection.anchorNode)) { + const range = document.createRange(); + if (editorNode.lastChild) { + range.setStartAfter(editorNode.lastChild); + } else { + range.setStart(editorNode, 0); + } + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + await nextTick(); + selection = window.getSelection(); } - - // 插入@提及 - insertMention(data) -} + + if (selection && selection.rangeCount > 0) { + insertMention(data, selection.getRangeAt(0).cloneRange()); + } +}; const onSubscribeQuote = (data) => { // 保存引用数据,但不保存到草稿中 @@ -1443,7 +1507,7 @@ const handleEditorClick = (event) => {

{{ nav.title }}

- +