From 54a46e2fb4899ea3add215d10ba8e554384eb7ef Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:51:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20=E4=BC=98=E5=8C=96=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=B9=B6?= =?UTF-8?q?=E6=B8=85=E7=90=86=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/editor/CustomEditor.vue | 554 ++++++++++++------------- 1 file changed, 270 insertions(+), 284 deletions(-) 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 = `
${data.title}
@@ -1055,34 +1054,34 @@ const onSubscribeQuote = (data) => {
×
` - // 将引用元素插入到编辑器开头 + if (editor.firstChild) { editor.insertBefore(quoteElement, editor.firstChild) } else { editor.appendChild(quoteElement) } - // 使用事件委托处理引用元素的所有点击事件 + quoteElement.addEventListener('click', (e) => { - // 检查点击的是否是关闭按钮或其内部元素 + const closeButton = e.target.classList?.contains('quote-close') ? e.target : e.target.closest('.quote-close') - // 阻止事件冒泡,避免触发编辑器的其他点击事件 + e.stopPropagation() if (closeButton) { - // 移除引用元素并清除引用数据 + quoteElement.remove() quoteData.value = null - // 只更新编辑器内容变量,不触发handleInput,避免保存草稿 + editorContent.value = editor.textContent || '' editorHtml.value = editor.innerHTML || '' - // 使用nextTick确保DOM更新后再设置焦点 + nextTick(() => editor.focus()) } else { - // 如果不是点击关闭按钮,则设置光标到引用卡片后面 + const selection = window.getSelection() const range = document.createRange() range.setStartAfter(quoteElement) @@ -1090,21 +1089,21 @@ const onSubscribeQuote = (data) => { selection.removeAllRanges() selection.addRange(range) - // 立即设置焦点 + editor.focus() } }) - // 注意:不调用saveDraft(),确保引用内容不会被保存到草稿中 - // 在同一个事件监听器中处理引用卡片的点击 - // 已经在上面的事件处理中添加了关闭按钮的处理逻辑 - // 这里只需要处理非关闭按钮的点击 - // 注意:由于事件委托的方式,不需要额外添加点击事件监听器 - // 监听键盘事件,处理引用元素的删除操作 + + + + + + const handleDeleteQuote = function(e) { - // 只处理删除键(Backspace 或 Delete) + if (e.key !== 'Backspace' && e.key !== 'Delete') return; const selection = window.getSelection(); @@ -1114,15 +1113,15 @@ const onSubscribeQuote = (data) => { const quoteElement = editor.querySelector('.editor-quote'); if (!quoteElement) { - // 如果引用元素已经被删除,移除事件监听器 + editor.removeEventListener('keydown', handleDeleteQuote); return; } - // 获取引用元素在子节点中的索引 + const quoteIndex = Array.from(editor.childNodes).indexOf(quoteElement); - // 检查光标是否在引用卡片前面(Backspace)或后面(Delete) + const isBeforeQuote = e.key === 'Backspace' && range.collapsed && range.startContainer === editor && @@ -1134,53 +1133,53 @@ const onSubscribeQuote = (data) => { quoteIndex === range.startOffset - 1; if (isBeforeQuote || isAfterQuote) { - // 阻止默认删除行为 + e.preventDefault(); - // 移除引用元素并清除引用数据 + quoteElement.remove(); quoteData.value = null; - // 更新编辑器内容 + handleInput({ target: editor }); } }; editor.addEventListener('keydown', handleDeleteQuote); - // 设置光标位置 + setTimeout(() => { - // 确保编辑器中有内容可以放置光标 + if (!editor.childNodes.length || (editor.childNodes.length === 1 && editor.childNodes[0] === quoteElement)) { - // 如果编辑器中只有引用元素,添加一个空的文本节点 - const textNode = document.createTextNode('\u200B'); // 使用零宽空格 + + const textNode = document.createTextNode('\u200B'); editor.appendChild(textNode); } - // 强制刷新编辑器内容,确保DOM已更新 + const currentHtml = editor.innerHTML; editor.innerHTML = currentHtml; - // 重新获取引用元素(因为innerHTML操作会导致之前的引用丢失) + const newQuoteElement = editor.querySelector('.editor-quote'); editor.focus(); const newSelection = window.getSelection(); const newRange = document.createRange(); - // 始终将光标设置在引用元素后面 + if (newQuoteElement) { - // 找到引用元素后的第一个节点 + let nextNode = newQuoteElement.nextSibling; - if (nextNode && nextNode.nodeType === 3) { // 文本节点 + if (nextNode && nextNode.nodeType === 3) { newRange.setStart(nextNode, 0); - } else if (nextNode) { // 其他元素节点 + } else if (nextNode) { newRange.setStartBefore(nextNode); - } else { // 没有下一个节点 + } else { newRange.setStartAfter(newQuoteElement); } } else { - // 如果找不到引用元素,则设置到编辑器开头 + if (editor.firstChild) { newRange.setStartBefore(editor.firstChild); } else { @@ -1192,16 +1191,16 @@ setTimeout(() => { newSelection.removeAllRanges(); newSelection.addRange(newRange); - // 确保光标可见 + editor.scrollTop = editor.scrollHeight; - // 更新编辑器内容 + handleInput({ target: editor }); - // 再次确保编辑器获得焦点 + setTimeout(() => { editor.focus(); - // 再次尝试设置光标位置 + if (newSelection.rangeCount === 0) { const finalRange = document.createRange(); if (newQuoteElement) { @@ -1214,14 +1213,14 @@ setTimeout(() => { newSelection.addRange(finalRange); } }, 50); -}, 200); // 增加延时确保DOM已完全更新 +}, 200); } const onSubscribeEdit = (data) => { editingMessage.value = data clearEditor() - // 插入编辑内容 + if (data.content) { editorRef.value.innerHTML = data.content editorContent.value = data.content @@ -1229,94 +1228,81 @@ const onSubscribeEdit = (data) => { } } -// 清除编辑器内容的事件处理函数 + const onSubscribeClear = () => { clearEditor() } -// 保存草稿 + const saveDraft = () => { if (!indexName.value || !editorRef.value) return - // 获取编辑器内容,但排除引用元素 - // 使用DocumentFragment进行高效的DOM操作 + + const fragment = document.createDocumentFragment() const tempDiv = document.createElement('div') tempDiv.innerHTML = editorRef.value.innerHTML fragment.appendChild(tempDiv) - // 从临时DOM中移除引用元素 + const quoteElements = tempDiv.querySelectorAll('.editor-quote') quoteElements.forEach(quote => quote.remove()) - // 获取不包含引用的内容 + const contentToSave = tempDiv.textContent || '' const htmlToSave = tempDiv.innerHTML || '' const currentEditor= parseEditorContent().items - // 检查是否有实际内容(不包括引用) + const hasContent = contentToSave.trim().length > 0 || htmlToSave.includes('0) { - console.log('保存到草稿',currentEditor.map(x=>{ - let text='' - if(x.type===3){ - text='[图片]' - }else if(x.type===1){ - text=x.content - } - return text - })?.join('')) - // 保存草稿到store,不包括引用数据 + editorDraftStore.items[indexName.value] = JSON.stringify({ - content: currentEditor.map(x=>{ - let text='' - if(x.type===3){ - text='[图片]' - }else if(x.type===1){ - text=x.content - } - return text - })?.join(''), + content: currentEditor.reduce((result, x) => { + if (x.type === 3) return result + '[图片]' + if (x.type === 1) return result + x.content + return result +}, ''), html: htmlToSave }) } else { - // 编辑器为空时删除对应草稿 + delete editorDraftStore.items[indexName.value] } } -// 加载草稿 + const loadDraft = () => { if (!indexName.value) return - // 使用nextTick确保DOM已渲染,更可预测且性能更好 + nextTick(() => { - // 保存当前引用数据的临时副本 + const currentQuoteData = quoteData.value - // 清除当前引用数据,避免重复添加 + quoteData.value = null - // 如果编辑器引用不存在,直接返回 + if (!editorRef.value) return - // 先清空编辑器内容 + editorRef.value.innerHTML = '' editorContent.value = '' editorHtml.value = '' - // 获取草稿数据 + const draft = editorDraftStore.items[indexName.value] - // 如果有草稿,恢复草稿内容 + if (draft) { try { const draftData = JSON.parse(draft) - // 恢复草稿内容 + editorRef.value.innerHTML = draftData.html || '' editorContent.value = draftData.content || '' editorHtml.value = draftData.html || '' @@ -1325,26 +1311,26 @@ const loadDraft = () => { } } - // 如果有引用数据,重新添加到编辑器(无论是否有草稿) + if (currentQuoteData) { onSubscribeQuote(currentQuoteData) } }) } -// 监听会话变化,加载对应草稿 + watch(indexName, loadDraft, { immediate: true }) -// 处理点击文档事件,隐藏@提及列表 + const handleDocumentClick = (event) => { if (!editorRef.value?.contains(event.target)) { hideMentionList() } } -// 组件挂载 + onMounted(() => { - // 订阅所有编辑器相关事件 + const subscriptions = [ [EditorConst.Mention, onSubscribeMention], [EditorConst.Quote, onSubscribeQuote], @@ -1352,18 +1338,18 @@ onMounted(() => { [EditorConst.Clear, onSubscribeClear] ] - // 批量订阅事件 + subscriptions.forEach(([event, handler]) => { bus.subscribe(event, handler) }) - // 为编辑器添加点击事件监听器 + editorRef.value?.addEventListener('click', handleEditorClick) - // 点击外部隐藏mention - 使用命名函数便于清理 + document.addEventListener('click', handleDocumentClick) - // 初始加载草稿 + loadDraft() }) @@ -1372,7 +1358,7 @@ onMounted(() => { * 清理所有事件订阅和监听器,防止内存泄漏 */ onBeforeUnmount(() => { - // 取消订阅所有编辑器相关事件 + const subscriptions = [ [EditorConst.Mention, onSubscribeMention], [EditorConst.Quote, onSubscribeQuote], @@ -1380,15 +1366,15 @@ onBeforeUnmount(() => { [EditorConst.Clear, onSubscribeClear] ] - // 批量取消订阅 + subscriptions.forEach(([event, handler]) => { bus.unsubscribe(event, handler) }) - // 移除编辑器点击事件监听器 + editorRef.value?.removeEventListener('click', handleEditorClick) - // 移除文档点击事件监听器 + document.removeEventListener('click', handleDocumentClick) const editor = editorRef.value if (editor && handleDeleteQuote) { @@ -1406,7 +1392,7 @@ onBeforeUnmount(() => { * 然后关闭表情选择器面板 */ const onEmoticonSelect = (emoji) => { - // 直接调用onEmoticonEvent处理表情 + onEmoticonEvent(emoji) isShowEmoticon.value = false } @@ -1447,7 +1433,7 @@ const onVoteSubmit = (data) => { isShowVote.value = false } -// 处理编辑器内部点击事件(用于关闭引用等) + const handleEditorClick = (event) => { const closeButton = event.target.closest('.quote-close'); @@ -1651,7 +1637,7 @@ const handleEditorClick = (event) => { width: 35px; margin: 0 2px; position: relative; - user-select: none; /* 防止文本被选中 */ + user-select: none; /** * 提示文本样式 @@ -1670,9 +1656,9 @@ const handleEditorClick = (event) => { font-size: 12px; padding: 0 5px; border-radius: 2px; - white-space: pre; /* 保留空白符 */ + white-space: pre; user-select: none; - z-index: 999999999999; /* 确保提示显示在最上层 */ + z-index: 999999999999; } /** @@ -1697,12 +1683,12 @@ const handleEditorClick = (event) => { color: #462AA0; text-decoration: none; position: relative; - padding-right: 60px; /* 为文件大小信息留出空间 */ + padding-right: 60px; max-width: 100%; box-sizing: border-box; overflow: hidden; - text-overflow: ellipsis; /* 文本溢出时显示省略号 */ - white-space: nowrap; /* 防止文本换行 */ + text-overflow: ellipsis; + white-space: nowrap; &::after { content: attr(data-size); @@ -1721,7 +1707,7 @@ const handleEditorClick = (event) => { display: inline-block; width: 24px; height: 24px; - vertical-align: middle; /* 垂直居中对齐 */ + vertical-align: middle; margin: 0 2px; } :deep(.editor-quote) { @@ -1735,10 +1721,10 @@ const handleEditorClick = (event) => { display: flex; justify-content: space-between; align-items: flex-start; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* 轻微阴影效果 */ - cursor: pointer; /* 添加指针样式 */ - user-select: none; /* 防止文本选择 */ - transition: background-color 0.2s ease; /* 平滑过渡效果 */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; /** * 引用卡片悬停效果 @@ -1775,12 +1761,12 @@ const handleEditorClick = (event) => { */ .quote-content { color: var(--im-text-color, #333); - word-break: break-all; /* 在任意字符间断行 */ - white-space: normal; /* 允许正常换行 */ + word-break: break-all; + white-space: normal; overflow: hidden; - text-overflow: ellipsis; /* 文本溢出显示省略号 */ + text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 2; /* 限制显示2行 */ + -webkit-line-clamp: 2; -webkit-box-orient: vertical; } @@ -1792,7 +1778,7 @@ const handleEditorClick = (event) => { max-width: 100px; max-height: 60px; border-radius: 3px; - pointer-events: none; /* 防止图片被拖拽 */ + pointer-events: none; } /** @@ -1832,7 +1818,7 @@ const handleEditorClick = (event) => { background: transparent; overflow-y: auto; - /* 添加placeholder样式 */ + &:empty:before { content: attr(placeholder); color: #999; @@ -1880,7 +1866,7 @@ const handleEditorClick = (event) => { .custom-editor:empty::before { content: attr(placeholder); color: #999; - pointer-events: none; /* 防止占位符文本接收鼠标事件 */ + pointer-events: none; font-family: PingFang SC, Microsoft YaHei, 'Alibaba PuHuiTi 2.0 45' !important; } @@ -1919,16 +1905,16 @@ const handleEditorClick = (event) => { * 限制编辑器中插入的图片大小 * 添加圆角和鼠标指针样式 */ - // .editor-image { - // max-width: 300px; - // max-height: 200px; - // border-radius: 3px; - // background-color: #48484d; - // margin: 0px 2px; - // cursor: pointer; - // object-fit: contain; /* 保持原始比例 */ - // display: inline-block; /* 确保图片正确显示 */ - // } + + + + + + + + + + /** * 表情样式 @@ -1956,7 +1942,7 @@ const handleEditorClick = (event) => { } - /* 引用卡片样式 */ + .quote-card { background: #f5f5f5; border-left: 3px solid #1890ff; @@ -2059,16 +2045,16 @@ html[theme-mode='dark'] { * 保留滚动功能但隐藏滚动条的视觉显示 */ .hide-scrollbar { - /* Chrome, Safari, Edge */ + &::-webkit-scrollbar { width: 0; display: none; } - /* Firefox */ + scrollbar-width: none; - /* IE */ + -ms-overflow-style: none; } \ No newline at end of file