Merge branch 'xingyy' into dev
This commit is contained in:
commit
1ae317dbb3
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
<n-icon size="18" class="icon" :component="nav.icon" />
|
||||
<p class="tip-title">{{ nav.title }}</p>
|
||||
</div>
|
||||
<n-button class="w-80px h-30px ml-auto" type="primary">
|
||||
<n-button class="w-80px h-30px ml-auto" type="primary" @click="sendMessage">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<IosSend />
|
||||
@ -1483,7 +1558,7 @@ const handleEditorClick = (event) => {
|
||||
:key="member.user_id || member.id"
|
||||
class="cursor-pointer px-14px h-42px"
|
||||
:class="{ 'bg-#EEE9F9': index === selectedMentionIndex }"
|
||||
@mousedown.prevent="insertMention(member)"
|
||||
@mousedown.prevent="handleMentionSelectByMouse(member)"
|
||||
@mouseover="selectedMentionIndex = index"
|
||||
>
|
||||
<div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full">
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { reactive } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useDialogueStore } from '@/store/modules/dialogue.js'
|
||||
|
||||
interface IDropdown {
|
||||
@ -14,11 +15,10 @@ const isRevoke = (uid: any, item: any): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
const datetime = item.created_at.replace(/-/g, '/')
|
||||
|
||||
const time = new Date().getTime() - Date.parse(datetime)
|
||||
|
||||
return Math.floor(time / 1000 / 60) <= 2
|
||||
const messageTime = dayjs(item.created_at)
|
||||
const now = dayjs()
|
||||
const diffInMinutes = now.diff(messageTime, 'minute')
|
||||
return diffInMinutes <= 5
|
||||
}
|
||||
const dialogueStore = useDialogueStore()
|
||||
export function useMenu() {
|
||||
@ -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' })
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user