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