diff --git a/package.json b/package.json index 0b8bf58..53c5bd8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@vueuse/core": "^10.7.0", "ant-design-vue": "^4.2.6", "axios": "^1.6.2", + "dayjs": "^1.11.13", "highlight.js": "^11.5.0", "js-audio-recorder": "^1.0.7", "lodash-es": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e186607..f92b4b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: axios: specifier: ^1.6.2 version: 1.9.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 highlight.js: specifier: ^11.5.0 version: 11.11.1 diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index 0ec7906..78d3013 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('') @@ -100,6 +102,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 +122,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 !== '') { @@ -163,11 +167,30 @@ 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 } +// 处理鼠标点击选择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 @@ -187,65 +210,73 @@ const updateMentionPosition = (range) => { } // 插入mention -const insertMention = (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', member.id || member.user_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 + + // 从@符号开始删除,直到当前光标位置 + range.setStart(textNode, atIndex); + range.setEnd(textNode, offset); + range.deleteContents(); + + // 插入 mention 元素 + range.insertNode(mentionSpan); } 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); } - - // 触发输入事件以更新编辑器内容 - handleInput({ target: editorRef.value }) - - // 隐藏mention列表 - hideMentionList() -} + + // 在 mention 之后插入一个空格,并将光标移到空格之后 + const spaceNode = document.createTextNode('\u00A0'); // 使用不间断空格 + const currentParent = mentionSpan.parentNode; + if (currentParent) { + // 将空格节点插入到 mentionSpan 之后 + if (mentionSpan.nextSibling) { + currentParent.insertBefore(spaceNode, mentionSpan.nextSibling); + } else { + currentParent.appendChild(spaceNode); + } + // 设置光标到空格之后 + range.setStartAfter(spaceNode); + range.collapse(true); + } else { + // Fallback: 如果 mentionSpan 没有父节点(理论上不应该发生),则将光标设置在 mentionSpan 之后 + range.setStartAfter(mentionSpan); + range.collapse(true); + } + + selection.removeAllRanges(); + selection.addRange(range); + + editorRef.value?.focus(); // 确保编辑器在操作后仍有焦点 + + nextTick(() => { + handleInput({ target: editorRef.value }); + hideMentionList(); + }); +}; // 处理粘贴事件 const handlePaste = (event) => { @@ -343,12 +374,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 @@ -391,6 +425,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) { // 如果光标在元素节点中,检查前一个子节点 @@ -525,24 +578,29 @@ 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, 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) @@ -602,7 +660,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') { @@ -634,7 +692,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) textContent = '' } @@ -674,7 +732,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) textContent = '' } @@ -696,7 +754,7 @@ const parseEditorContent = () => { if (textContent.trim()) { items.push({ type: 1, - content: textContent.trim() + content: textContent.trimEnd() }) } @@ -918,27 +976,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) => { // 保存引用数据,但不保存到草稿中 @@ -1431,7 +1506,7 @@ const handleEditorClick = (event) => {

{{ nav.title }}

- +