diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index d18a6f4..afb7e99 100644 --- a/src/components/editor/CustomEditor.vue +++ b/src/components/editor/CustomEditor.vue @@ -51,10 +51,10 @@ const emoticonRef = ref(null) const dialogueStore = useDialogueStore() const editorDraftStore = useEditorDraftStore() -// 当前会话索引 + const indexName = computed(() => dialogueStore.index_name) -// 工具栏配置 + const navs = ref([ { title: '图片', @@ -79,11 +79,11 @@ const currentMentionQuery = ref('') setTimeout(() => { console.log('props.members',props.members) }, 1000) -// 编辑器内容 + const editorContent = ref('') const editorHtml = ref('') -// 工具栏配置 + const toolbarConfig = computed(() => { const config = [ { type: 'emoticon', icon: 'icon-biaoqing', title: '表情' }, @@ -94,21 +94,21 @@ const toolbarConfig = computed(() => { return config }) -// 处理输入事件 + const handleInput = (event) => { const target = event.target - // 获取编辑器内容,但不包括引用元素的内容 + 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') let textContent = editorClone.textContent || '' - // 将表情图片的 alt 属性添加到文本内容中 + if (emojiImages.length > 0) { emojiImages.forEach(emoji => { const altText = emoji.getAttribute('alt') @@ -118,11 +118,11 @@ const handleInput = (event) => { }) } - // 更新逻辑文本内容 + editorContent.value = textContent - // 检查是否需要清空编辑器以显示placeholder - // 只有当编辑器中没有任何内容(包括空格)且没有其他元素时才清空 + + const isEmpty = textContent === '' && !target.querySelector('img, .editor-file, .mention') @@ -130,28 +130,24 @@ const handleInput = (event) => { target.innerHTML = '' } - // 更新HTML内容 + editorHtml.value = target.innerHTML || '' const currentEditor= parseEditorContent().items - // 后续操作 + checkMention(target) saveDraft() emit('editor-event', { event: 'input_event', - data: currentEditor.map(x=>{ - let text='' - if(x.type===3){ - text='[图片]' - }else if(x.type===1){ - text=x.content - } - return text - })?.join('') + data: currentEditor.reduce((result, x) => { + if (x.type === 3) return result + '[图片]' + if (x.type === 1) return result + x.content + return result +}, '') }) } -// 检查@mention + const checkMention = (target) => { const selection = window.getSelection() if (!selection.rangeCount) return @@ -169,7 +165,7 @@ const checkMention = (target) => { } } -// 显示mention列表 + const showMentionList = () => { const query = currentMentionQuery.value.toLowerCase() mentionList.value = props.members.filter(member => { @@ -182,13 +178,13 @@ 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(); @@ -199,14 +195,14 @@ const handleMentionSelectByMouse = (member) => { } }; -// 隐藏mention列表 + const hideMentionList = () => { showMention.value = false mentionList.value = [] currentMentionQuery.value = '' } -// 更新mention位置 + const updateMentionPosition = (range) => { const rect = range.getBoundingClientRect() const editorRect = editorRef.value.getBoundingClientRect() @@ -217,18 +213,18 @@ const updateMentionPosition = (range) => { } } -// 插入mention + const insertMention = (member, clonedRange) => { console.log('插入mention', member); const selection = window.getSelection(); if (!clonedRange || !selection) return; - const range = clonedRange; // 使用传入的克隆 range + const range = clonedRange; 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'); @@ -239,38 +235,38 @@ const insertMention = (member, clonedRange) => { if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) { const parent = textNode.parentNode; - if (!parent) return; // Sanity check + if (!parent) return; - // 从@符号开始删除,直到当前光标位置 + range.setStart(textNode, atIndex); range.setEnd(textNode, offset); range.deleteContents(); - // 插入 mention 元素 + range.insertNode(mentionSpan); } else { - // 如果没有找到@符号,或者光标不在合适的文本节点内,直接在当前光标位置插入 + if (!range.collapsed) { range.deleteContents(); } range.insertNode(mentionSpan); } - // 在 mention 之后插入一个空格,并将光标移到空格之后 - const spaceNode = document.createTextNode('\u00A0'); // 使用不间断空格 + + 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); } @@ -278,7 +274,7 @@ const insertMention = (member, clonedRange) => { selection.removeAllRanges(); selection.addRange(range); - editorRef.value?.focus(); // 确保编辑器在操作后仍有焦点 + editorRef.value?.focus(); nextTick(() => { handleInput({ target: editorRef.value }); @@ -286,16 +282,16 @@ const insertMention = (member, clonedRange) => { }); }; -// 处理粘贴事件 + const handlePaste = (event) => { event.preventDefault() - // 检查是否有图片 + const items = event.clipboardData?.items if (items) { for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { - // 获取粘贴的图片文件 + const file = items[i].getAsFile() if (file) { const tempUrl = URL.createObjectURL(file); @@ -327,11 +323,11 @@ const handlePaste = (event) => { } } - // 获取粘贴的纯文本内容 + const text = event.clipboardData?.getData('text/plain') || '' if (text) { - // 插入纯文本,移除所有样式 + const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) @@ -341,21 +337,21 @@ const handlePaste = (event) => { selection.removeAllRanges() selection.addRange(range) - // 触发输入事件 + handleInput({ target: editorRef.value }) } } } -// 处理键盘事件 + const handleKeydown = (event) => { - // 处理@提及列表的键盘导航 + if (showMention.value) { switch (event.key) { case 'ArrowUp': event.preventDefault() selectedMentionIndex.value = Math.max(0, selectedMentionIndex.value - 1) - // 确保选中项可见 - 向上滚动 + nextTick(() => { const mentionList = document.querySelector('.mention-list ul') const selectedItem = mentionList?.children[selectedMentionIndex.value] @@ -367,7 +363,7 @@ const handleKeydown = (event) => { case 'ArrowDown': event.preventDefault() selectedMentionIndex.value = Math.min(mentionList.value.length - 1, selectedMentionIndex.value + 1) - // 确保选中项可见 - 向下滚动 + nextTick(() => { const mentionList = document.querySelector('.mention-list ul') const selectedItem = mentionList?.children[selectedMentionIndex.value] @@ -398,7 +394,7 @@ const handleKeydown = (event) => { return } - // 处理删除键(Backspace和Delete)删除mention元素 + if (event.key === 'Backspace' || event.key === 'Delete') { const selection = window.getSelection() if (!selection.rangeCount) return @@ -406,18 +402,18 @@ const handleKeydown = (event) => { const range = selection.getRangeAt(0) const editor = editorRef.value - // 只处理光标位置的删除,不处理选中内容的删除 + if (range.collapsed) { let targetMention = null - // 获取光标位置信息 + const container = range.startContainer const offset = range.startOffset if (event.key === 'Backspace') { - // Backspace:查找光标前面的mention元素 + if (container.nodeType === Node.TEXT_NODE) { - // 如果光标在文本节点的开头,检查前一个兄弟节点 + if (offset === 0) { let prevSibling = container.previousSibling while (prevSibling) { @@ -427,15 +423,15 @@ const handleKeydown = (event) => { targetMention = prevSibling break } - // 如果是文本节点且不为空,停止查找 + if (prevSibling.nodeType === Node.TEXT_NODE && prevSibling.textContent.trim()) { break } prevSibling = prevSibling.previousSibling } } else { - // 如果光标在文本节点中间或末尾,且当前文本节点只包含空格 - // 检查前一个兄弟节点是否是mention + + if (!container.textContent.trim()) { let prevSibling = container.previousSibling while (prevSibling) { @@ -445,7 +441,7 @@ const handleKeydown = (event) => { targetMention = prevSibling break } - // 如果是文本节点且不为空,停止查找 + if (prevSibling.nodeType === Node.TEXT_NODE && prevSibling.textContent.trim()) { break } @@ -454,15 +450,15 @@ const handleKeydown = (event) => { } } } else if (container.nodeType === Node.ELEMENT_NODE) { - // 如果光标在元素节点中,检查前一个子节点 + if (offset > 0) { let prevChild = container.childNodes[offset - 1] - // 如果前一个子节点是mention元素 + if (prevChild && prevChild.nodeType === Node.ELEMENT_NODE && prevChild.classList && prevChild.classList.contains('mention')) { targetMention = prevChild } - // 如果前一个子节点是文本节点,检查它前面的兄弟节点 + else if (prevChild && prevChild.nodeType === Node.TEXT_NODE && !prevChild.textContent.trim()) { let prevSibling = prevChild.previousSibling while (prevSibling) { @@ -479,7 +475,7 @@ const handleKeydown = (event) => { } } } else if (offset === 0 && container === editor) { - // 特殊情况:光标在编辑器开头,检查第一个子节点是否是mention + const firstChild = container.firstChild if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.classList && firstChild.classList.contains('mention')) { @@ -488,9 +484,9 @@ const handleKeydown = (event) => { } } } else if (event.key === 'Delete') { - // Delete:查找光标后面的mention元素 + if (container.nodeType === Node.TEXT_NODE) { - // 如果光标在文本节点的末尾,检查后一个兄弟节点 + if (offset === container.textContent.length) { let nextSibling = container.nextSibling while (nextSibling) { @@ -500,7 +496,7 @@ const handleKeydown = (event) => { targetMention = nextSibling break } - // 如果是文本节点且不为空,停止查找 + if (nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim()) { break } @@ -508,7 +504,7 @@ const handleKeydown = (event) => { } } } else if (container.nodeType === Node.ELEMENT_NODE) { - // 如果光标在元素节点中,检查后一个子节点 + if (offset < container.childNodes.length) { const nextChild = container.childNodes[offset] if (nextChild && nextChild.nodeType === Node.ELEMENT_NODE && @@ -519,23 +515,23 @@ const handleKeydown = (event) => { } } - // 如果找到了要删除的mention元素 + if (targetMention) { event.preventDefault() - // 删除mention元素 + targetMention.remove() - // 触发输入事件更新编辑器内容 + handleInput({ target: editor }) return } } } - // 处理Ctrl+Enter或Shift+Enter换行 + if (event.key === 'Enter' && (event.ctrlKey || event.metaKey || event.shiftKey)) { - // 手动插入换行符 + const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) @@ -543,7 +539,7 @@ const handleKeydown = (event) => { range.deleteContents() range.insertNode(br) - // 在换行符后添加一个空文本节点,并将光标移动到这个节点 + const textNode = document.createTextNode('') range.setStartAfter(br) range.insertNode(textNode) @@ -552,51 +548,54 @@ const handleKeydown = (event) => { selection.removeAllRanges() selection.addRange(range) - // 触发输入事件更新编辑器内容 + handleInput({ target: editorRef.value }) } - // 阻止默认行为,防止触发表单提交 + event.preventDefault() return } - // 处理Enter键发送消息(只有在没有按Ctrl/Cmd/Shift时才发送) + if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey && !event.shiftKey) { event.preventDefault() - // 检查引用元素是否存在,如果不存在但 quoteData 有值,则清除 quoteData + const editor = editorRef.value const quoteElement = editor?.querySelector('.editor-quote') if (!quoteElement && quoteData.value) { quoteData.value = null } - // 解析编辑器内容并发送消息 + sendMessage() } } -// 发送消息 + const sendMessage = () => { - // 解析编辑器内容 + const messageData = parseEditorContent() - // 检查是否有内容可发送 + if (messageData.items.length === 0 || (messageData.items.length === 1 && messageData.items[0].type === 1 && !messageData.items[0].content.trimEnd())) { - return // 没有内容,不发送 + return } - // 处理不同类型的消息 + + function cleanInvisibleChars(text) { + return text.replace(/[\u200B-\u200D\uFEFF]/g, ''); +} messageData.items.forEach(item => { - // 处理文本内容 - if (item.type === 1 && item.content.trimEnd()) { + + if (item.type === 1 && cleanInvisibleChars(item.content.trimEnd())) { const data = { items: [{ - content: item.content, + content: cleanInvisibleChars(item.content), type: 1 }], mentionUids: messageData.mentionUids, @@ -613,90 +612,90 @@ const sendMessage = () => { 'editor-event', emitCall('text_event', data) ) - } else if (item.type === 3) { // 图片消息 + } else if (item.type === 3) { const data = { height: 0, width: 0, size: 10000, url: item.content, } - + console.log('图片消息data',data) emit( 'editor-event', emitCall('image_event', data) ) - } else if (item.type === 4) { // 文件消息 - // 文件消息处理逻辑 + } else if (item.type === 4) { + } }) - // 发送后清空编辑器 + clearEditor() } -// 解析编辑器内容 + const parseEditorContent = () => { const items = [] const mentionUids = [] - // 解析HTML内容 + const tempDiv = document.createElement('div') tempDiv.innerHTML = editorHtml.value - // 检查是否有引用元素 + const quoteElements = tempDiv.querySelectorAll('.editor-quote') const hasQuote = quoteElements.length > 0 && quoteData.value const quoteId = hasQuote ? quoteData.value.id || '' : '' - // 移除引用元素,避免重复处理 + quoteElements.forEach(quote => quote.remove()) let textContent = '' - // 处理文本节点和元素节点 + const processNode = (node) => { if (node.nodeType === Node.TEXT_NODE) { - // 文本节点直接添加内容 + textContent += node.textContent return } if (node.nodeType !== Node.ELEMENT_NODE) return - // 处理不同类型的元素 + if (node.classList.contains('mention')) { - // 处理@提及 + const userId = node.getAttribute('data-user-id') if (userId) { mentionUids.push(Number(userId)) } textContent += node.textContent } else if (node.tagName === 'IMG') { - // 处理图片 + processImage(node) } else if (node.classList.contains('emoji')) { - // 处理emoji元素 + textContent += node.getAttribute('alt') || node.textContent } else if (node.classList.contains('editor-file')) { - // 处理文件 + processFile(node) } else if (node.childNodes.length) { - // 处理有子节点的元素 + Array.from(node.childNodes).forEach(processNode) } else { - // 其他元素,添加文本内容 + textContent += node.textContent } } - // 处理图片元素 + const processImage = (node) => { const src = node.getAttribute('src') const width = node.getAttribute('data-original-width') || node.getAttribute('width') || '' const height = node.getAttribute('data-original-height') || node.getAttribute('height') || '' const isEmoji = node.classList.contains('editor-emoji') - // 如果有累积的文本内容,先添加到items + if (textContent.trim()) { items.push({ type: 1, @@ -706,13 +705,13 @@ const parseEditorContent = () => { } if (isEmoji) { - // 处理表情图片 + const altText = node.getAttribute('alt') || '' if (altText) { - // 如果有alt文本,将表情作为文本处理 + textContent += altText } else { - // 否则作为图片处理 + items.push({ type: 3, content: src + (width && height ? `?width=${width}&height=${height}` : ''), @@ -720,7 +719,7 @@ const parseEditorContent = () => { }) } } else { - // 处理普通图片 + items.push({ type: 3, content: src + (width && height ? `?width=${width}&height=${height}` : ''), @@ -730,13 +729,13 @@ const parseEditorContent = () => { } } - // 处理文件元素 + const processFile = (node) => { const fileUrl = node.getAttribute('data-url') const fileName = node.getAttribute('data-name') const fileSize = node.getAttribute('data-size') - // 如果有累积的文本内容,先添加到items + if (textContent.trim()) { items.push({ type: 1, @@ -747,7 +746,7 @@ const parseEditorContent = () => { if (fileUrl && fileName) { items.push({ - type: 4, // 使用数字类型保持一致性 + type: 4, content: fileUrl, name: fileName, size: node.getAttribute('data-size-raw') || fileSize || 0 @@ -755,10 +754,10 @@ const parseEditorContent = () => { } } - // 处理所有顶级节点 + Array.from(tempDiv.childNodes).forEach(processNode) - // 处理剩余的文本内容 + if (textContent.trim()) { items.push({ type: 1, @@ -766,7 +765,7 @@ const parseEditorContent = () => { }) } - // 构建完整的消息数据结构 + return { items: items.length > 0 ? items : [{ type: 1, content: '' }], mentionUids, @@ -774,27 +773,27 @@ const parseEditorContent = () => { } } -// 清空编辑器 + const clearEditor = () => { - // 一次性清空所有编辑器相关状态 + editorContent.value = '' editorHtml.value = '' quoteData.value = null - // 清空DOM内容 + if (editorRef.value) { editorRef.value.innerHTML = '' - // 立即设置焦点,提高响应速度 + nextTick(() => editorRef.value.focus()) } - // 隐藏@提及列表 + hideMentionList() - // 清空草稿 + saveDraft() - // 触发输入事件 + emit('editor-event', { event: 'input_event', data: '' @@ -803,23 +802,23 @@ const clearEditor = () => { -// 插入图片 + const insertImage = (src, width, height) => { const selection = window.getSelection() if (!selection.rangeCount) return const range = selection.getRangeAt(0) - // 创建图片元素 + const img = document.createElement('img') img.src = src img.className = 'editor-image' img.alt = '图片' img.style.maxHeight = '150px' img.style.maxWidth = '150px' - img.style.objectFit = 'contain' // 保持原始比例 + img.style.objectFit = 'contain' + - // 存储原始尺寸信息,但不直接设置宽高属性 if (width) img.setAttribute('data-original-width', width) if (height) img.setAttribute('data-original-height', height) @@ -834,7 +833,7 @@ const insertImage = (src, width, height) => { handleInput({ target: editorRef.value }) } -// 格式化文件大小 + const formatFileSize = (size) => { if (size < 1024) { return size + ' B' @@ -846,7 +845,7 @@ const formatFileSize = (size) => { return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB' } } -//工具栏中选完图片直接发送 + const onUploadSendImg=async (eventFile)=>{ for (const file of eventFile.target.files) { const form = new FormData(); @@ -875,49 +874,49 @@ async function onUploadFile(e) { const file = e.target.files[0] if (!file) return - // 清空input,允许再次选择相同文件 + e.target.value = null if (file.type.indexOf('image/') === 0) { - // 处理图片文件 - 立即显示临时消息,然后上传 + emit('editor-event', emitCall('image_event', file)) return } if (file.type.indexOf('video/') === 0) { - // 处理视频文件 + emit('editor-event', emitCall('video_event', file)) } else { - // 处理其他类型文件 + emit('editor-event', emitCall('file_event', file)) } } -// 表情选择事件 + const onEmoticonEvent = (emoji) => { - // 关闭表情面板 + emoticonRef.value?.setShow(false) - // 处理不同类型的表情 + switch (emoji.type) { case 'text': case 'emoji': - // 文本表情和系统表情都使用相同的处理方式 + insertTextEmoji(emoji.value) break case 'image': - // 图片表情 + insertImageEmoji(emoji.img, emoji.value) break - case 1: // 兼容旧版表情格式 + case 1: emoji.img ? insertImageEmoji(emoji.img, emoji.value) : insertTextEmoji(emoji.value) break default: - // 发送整个表情包 + emit('editor-event', { event: 'emoticon_event', data: emoji.value || emoji.id @@ -926,7 +925,7 @@ const onEmoticonEvent = (emoji) => { } } -// 插入文本表情 + const insertTextEmoji = (emojiText) => { const editor = editorRef.value if (!editor) return @@ -941,17 +940,17 @@ const insertTextEmoji = (emojiText) => { const textNode = document.createTextNode(emojiText) range.insertNode(textNode) - // 移动光标到插入文本之后 + range.setStartAfter(textNode) range.collapse(true) selection.removeAllRanges() selection.addRange(range) - // 触发输入事件以更新编辑器内容 + handleInput({ target: editor }) } -// 插入图片表情 + const insertImageEmoji = (imgSrc, altText) => { const editor = editorRef.value if (!editor) return @@ -966,30 +965,30 @@ const insertImageEmoji = (imgSrc, altText) => { const img = document.createElement('img') img.src = imgSrc img.alt = altText - img.className = 'editor-emoji' // 使用已有的样式 - // img.style.width = '24px' // 样式已在 .editor-emoji 中定义 - // img.style.height = '24px' - // img.style.verticalAlign = 'middle' + img.className = 'editor-emoji' + + + range.insertNode(img) - // 移动光标到插入图片之后 + range.setStartAfter(img) range.collapse(true) selection.removeAllRanges() selection.addRange(range) - // 触发输入事件以更新编辑器内容 + handleInput({ target: editor }) } -// 事件监听 + const onSubscribeMention = async (data) => { const editorNode = editorRef.value; if (!editorNode) return; editorNode.focus(); - await nextTick(); // 确保焦点已设置 + await nextTick(); let selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { @@ -1024,28 +1023,28 @@ const onSubscribeMention = async (data) => { }; const onSubscribeQuote = (data) => { - // 保存引用数据,但不保存到草稿中 + quoteData.value = data - // 在编辑器中显示引用内容 + const editor = editorRef.value if (!editor) return - // 先移除已有的引用元素 + const existingQuotes = editor.querySelectorAll('.editor-quote') existingQuotes.forEach(quote => quote.remove()) - // 保存当前光标位置 + const selection = window.getSelection() const savedRange = selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null const hasContent = editor.textContent.trim().length > 0 - // 创建引用元素 + const quoteElement = document.createElement('div') quoteElement.className = 'editor-quote' - quoteElement.contentEditable = 'false' // 设置为不可编辑 + quoteElement.contentEditable = 'false' + - // 添加引用内容和关闭按钮 quoteElement.innerHTML = `