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 }}
-