Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4b5c160e94 | ||
|
ebd567a757 | ||
18871db6b6 | |||
|
1ae317dbb3 | ||
|
e4354d42cd | ||
|
8bba2d64af | ||
|
d4e52152ef | ||
|
bdf07155c8 | ||
|
b905db0cfa | ||
|
3b6d998ce1 | ||
|
5340461a7e | ||
|
45eec2ff22 | ||
|
9c34066128 | ||
|
628894a254 | ||
92fce58429 | |||
|
2e998a1174 | ||
|
60a2fb996b | ||
b282562cdd | |||
d0abf7d8ab | |||
|
409af72039 | ||
|
799599bd83 | ||
ec18d85546 | |||
|
a97f293a6c |
@ -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
|
||||
|
@ -33,7 +33,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['editor-event'])
|
||||
|
||||
// 响应式数据
|
||||
const editorRef = ref(null)
|
||||
const content = ref('')
|
||||
const isFocused = ref(false)
|
||||
@ -49,7 +48,6 @@ const fileImageRef = ref(null)
|
||||
const uploadFileRef = ref(null)
|
||||
const emoticonRef = ref(null)
|
||||
|
||||
// 获取状态管理
|
||||
const dialogueStore = useDialogueStore()
|
||||
const editorDraftStore = useEditorDraftStore()
|
||||
|
||||
@ -78,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('')
|
||||
@ -94,7 +94,7 @@ const toolbarConfig = computed(() => {
|
||||
return config
|
||||
})
|
||||
|
||||
// 处理输入事件 - 优化版本,减少DOM操作
|
||||
// 处理输入事件
|
||||
const handleInput = (event) => {
|
||||
const target = event.target
|
||||
|
||||
@ -102,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')
|
||||
@ -121,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 !== '') {
|
||||
@ -130,14 +132,22 @@ const handleInput = (event) => {
|
||||
|
||||
// 更新HTML内容
|
||||
editorHtml.value = target.innerHTML || ''
|
||||
|
||||
const currentEditor= parseEditorContent().items
|
||||
// 后续操作
|
||||
checkMention(target)
|
||||
saveDraft()
|
||||
|
||||
emit('editor-event', {
|
||||
event: 'input_event',
|
||||
data: editorContent.value
|
||||
data: currentEditor.map(x=>{
|
||||
let text=''
|
||||
if(x.type===3){
|
||||
text='[图片]'
|
||||
}else if(x.type===1){
|
||||
text=x.content
|
||||
}
|
||||
return text
|
||||
})?.join('')
|
||||
})
|
||||
}
|
||||
|
||||
@ -162,14 +172,33 @@ const checkMention = (target) => {
|
||||
// 显示mention列表
|
||||
const showMentionList = () => {
|
||||
const query = currentMentionQuery.value.toLowerCase()
|
||||
mentionList.value = props.members.filter(member =>
|
||||
member.nickname.toLowerCase().includes(query)
|
||||
).slice(0, 10)
|
||||
|
||||
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
|
||||
@ -189,65 +218,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) => {
|
||||
@ -310,7 +347,7 @@ const handlePaste = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理键盘事件 - 优化版本,减少不必要的日志和简化逻辑
|
||||
// 处理键盘事件
|
||||
const handleKeydown = (event) => {
|
||||
// 处理@提及列表的键盘导航
|
||||
if (showMention.value) {
|
||||
@ -345,12 +382,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
|
||||
@ -358,6 +398,141 @@ const handleKeydown = (event) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理删除键(Backspace和Delete)删除mention元素
|
||||
if (event.key === 'Backspace' || event.key === 'Delete') {
|
||||
const selection = window.getSelection()
|
||||
if (!selection.rangeCount) return
|
||||
|
||||
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) {
|
||||
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 {
|
||||
// 如果光标在文本节点中间或末尾,且当前文本节点只包含空格
|
||||
// 检查前一个兄弟节点是否是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) {
|
||||
// 如果光标在元素节点中,检查前一个子节点
|
||||
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) {
|
||||
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 (offset === 0 && container === editor) {
|
||||
// 特殊情况:光标在编辑器开头,检查第一个子节点是否是mention
|
||||
const firstChild = container.firstChild
|
||||
if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE &&
|
||||
firstChild.classList && firstChild.classList.contains('mention')) {
|
||||
targetMention = firstChild
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Delete') {
|
||||
// Delete:查找光标后面的mention元素
|
||||
if (container.nodeType === Node.TEXT_NODE) {
|
||||
// 如果光标在文本节点的末尾,检查后一个兄弟节点
|
||||
if (offset === container.textContent.length) {
|
||||
let nextSibling = container.nextSibling
|
||||
while (nextSibling) {
|
||||
if (nextSibling.nodeType === Node.ELEMENT_NODE &&
|
||||
nextSibling.classList &&
|
||||
nextSibling.classList.contains('mention')) {
|
||||
targetMention = nextSibling
|
||||
break
|
||||
}
|
||||
// 如果是文本节点且不为空,停止查找
|
||||
if (nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim()) {
|
||||
break
|
||||
}
|
||||
nextSibling = nextSibling.nextSibling
|
||||
}
|
||||
}
|
||||
} else if (container.nodeType === Node.ELEMENT_NODE) {
|
||||
// 如果光标在元素节点中,检查后一个子节点
|
||||
if (offset < container.childNodes.length) {
|
||||
const nextChild = container.childNodes[offset]
|
||||
if (nextChild && nextChild.nodeType === Node.ELEMENT_NODE &&
|
||||
nextChild.classList && nextChild.classList.contains('mention')) {
|
||||
targetMention = nextChild
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了要删除的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)) {
|
||||
// 手动插入换行符
|
||||
@ -402,7 +577,7 @@ const handleKeydown = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息 - 优化版本,移除不必要的日志,简化逻辑
|
||||
// 发送消息
|
||||
const sendMessage = () => {
|
||||
// 解析编辑器内容
|
||||
const messageData = parseEditorContent()
|
||||
@ -411,24 +586,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)
|
||||
@ -488,7 +668,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') {
|
||||
@ -520,7 +700,7 @@ const parseEditorContent = () => {
|
||||
if (textContent.trim()) {
|
||||
items.push({
|
||||
type: 1,
|
||||
content: textContent.trim()
|
||||
content: textContent.trimEnd()
|
||||
})
|
||||
textContent = ''
|
||||
}
|
||||
@ -560,7 +740,7 @@ const parseEditorContent = () => {
|
||||
if (textContent.trim()) {
|
||||
items.push({
|
||||
type: 1,
|
||||
content: textContent.trim()
|
||||
content: textContent.trimEnd()
|
||||
})
|
||||
textContent = ''
|
||||
}
|
||||
@ -582,7 +762,7 @@ const parseEditorContent = () => {
|
||||
if (textContent.trim()) {
|
||||
items.push({
|
||||
type: 1,
|
||||
content: textContent.trim()
|
||||
content: textContent.trimEnd()
|
||||
})
|
||||
}
|
||||
|
||||
@ -804,27 +984,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) => {
|
||||
// 保存引用数据,但不保存到草稿中
|
||||
@ -1055,17 +1252,34 @@ const saveDraft = () => {
|
||||
// 获取不包含引用的内容
|
||||
const contentToSave = tempDiv.textContent || ''
|
||||
const htmlToSave = tempDiv.innerHTML || ''
|
||||
|
||||
const currentEditor= parseEditorContent().items
|
||||
// 检查是否有实际内容(不包括引用)
|
||||
const hasContent = contentToSave.trim().length > 0 ||
|
||||
htmlToSave.includes('<img') ||
|
||||
htmlToSave.includes('editor-file')
|
||||
|
||||
// 根据内容状态保存或删除草稿
|
||||
if (hasContent) {
|
||||
if (currentEditor.length>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: contentToSave,
|
||||
content: currentEditor.map(x=>{
|
||||
let text=''
|
||||
if(x.type===3){
|
||||
text='[图片]'
|
||||
}else if(x.type===1){
|
||||
text=x.content
|
||||
}
|
||||
return text
|
||||
})?.join(''),
|
||||
html: htmlToSave
|
||||
})
|
||||
} else {
|
||||
@ -1317,7 +1531,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 />
|
||||
@ -1369,7 +1583,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">
|
||||
|
@ -686,18 +686,13 @@ const fileTypeAvatar = (fileType) => {
|
||||
|
||||
const previewPDF = (item) => {
|
||||
console.log(item)
|
||||
// if (typeof plus !== 'undefined') {
|
||||
// downloadAndOpenFile(item)
|
||||
// } else {
|
||||
// document.addEventListener('plusready', () => {
|
||||
// downloadAndOpenFile(item)
|
||||
// })
|
||||
// }
|
||||
window.open(
|
||||
`${import.meta.env.VITE_PAGE_URL}/office?url=${item.extra.path}`,
|
||||
'_blank',
|
||||
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
|
||||
)
|
||||
if (typeof plus !== 'undefined') {
|
||||
downloadAndOpenFile(item)
|
||||
} else {
|
||||
document.addEventListener('plusready', () => {
|
||||
downloadAndOpenFile(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadAndOpenFile = (item) => {
|
||||
@ -931,6 +926,7 @@ body:deep(.round-3) {
|
||||
}
|
||||
.condition-each-resultList {
|
||||
.condition-each-resultList-each {
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
.condition-each-result-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -946,16 +942,9 @@ body:deep(.round-3) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 14px 20px;
|
||||
padding: 14px 0;
|
||||
// background-color: #f3f3f3;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(70, 41, 157, 0.1)
|
||||
}
|
||||
|
||||
.attachment-avatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -1137,10 +1126,6 @@ body:deep(.round-3) {
|
||||
.image-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid #46299d;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-image) {
|
||||
|
@ -69,43 +69,7 @@
|
||||
class="text-[12px] font-regular"
|
||||
:text="resultDetail"
|
||||
:searchText="props.searchText"
|
||||
v-if="props.searchItem?.msg_type !== 3 && props.searchItem?.msg_type !== 6"
|
||||
/>
|
||||
<div class="message-component-wrapper" v-if="props.searchItem?.msg_type === 3" @click.stop>
|
||||
<component
|
||||
:is="MessageComponents[props.searchItem?.msg_type] || 'unknown-message'"
|
||||
:extra="resultDetail"
|
||||
:data="props?.searchItem"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-message-wrapper" v-if="props.searchItem?.msg_type === 6" @click.stop>
|
||||
<div class="condition-each-result-attachments" @click="previewPDF(resultDetail.path)">
|
||||
<div class="attachment-avatar">
|
||||
<img :src="resultDetail?.file_avatar" />
|
||||
</div>
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-info-title">
|
||||
<span class="text-[14px] font-regular">
|
||||
{{ resultDetail?.name }}
|
||||
</span>
|
||||
<span
|
||||
class="text-[14px] font-regular"
|
||||
style="color: #999999; flex-shrink: 0; margin: 0 0 0 20px;"
|
||||
>
|
||||
{{ resultDetail?.dateTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="attachment-sub-info">
|
||||
<span class="text-[12px] font-regular">
|
||||
{{ resultDetail?.typeText }}
|
||||
</span>
|
||||
<span class="text-[12px] font-regular" style="flex-shrink: 0; margin: 0 0 0 20px;">
|
||||
{{ resultDetail?.fileSize }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchRecordDetail-fastLocal" v-if="searchRecordDetail">
|
||||
<span>定位到聊天位置</span>
|
||||
</div>
|
||||
@ -121,7 +85,7 @@ import avatarModule from '@/components/avatar-module/index.vue'
|
||||
import { ref, watch, computed, onMounted, onUnmounted, reactive, defineProps } from 'vue'
|
||||
import HighlightText from './highLightText.vue'
|
||||
import { beautifyTime } from '@/utils/datetime'
|
||||
import { ChatMsgTypeMapping, MessageComponents } from '@/constant/message'
|
||||
import { ChatMsgTypeMapping } from '@/constant/message'
|
||||
const props = defineProps({
|
||||
searchItem: Object | Number,
|
||||
searchResultKey: {
|
||||
@ -291,8 +255,6 @@ const resultDetail = computed(() => {
|
||||
result_detail =
|
||||
props.searchItem?.msg_type === 1
|
||||
? props.searchItem?.extra?.content
|
||||
: props.searchItem?.msg_type === 3 || props.searchItem?.msg_type === 6
|
||||
? props.searchItem?.extra
|
||||
: ChatMsgTypeMapping[props.searchItem?.msg_type]
|
||||
break
|
||||
default:
|
||||
@ -300,22 +262,6 @@ const resultDetail = computed(() => {
|
||||
}
|
||||
return result_detail
|
||||
})
|
||||
|
||||
const previewPDF = (item) => {
|
||||
console.log(item)
|
||||
// if (typeof plus !== 'undefined') {
|
||||
// downloadAndOpenFile(item)
|
||||
// } else {
|
||||
// document.addEventListener('plusready', () => {
|
||||
// downloadAndOpenFile(item)
|
||||
// })
|
||||
// }
|
||||
window.open(
|
||||
`${import.meta.env.VITE_PAGE_URL}/office?url=${item}`,
|
||||
'_blank',
|
||||
'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-item {
|
||||
@ -375,69 +321,6 @@ const previewPDF = (item) => {
|
||||
color: #999999;
|
||||
line-height: 20px;
|
||||
}
|
||||
.file-message-wrapper {
|
||||
.condition-each-result-attachments {
|
||||
width: 289px;
|
||||
height: 62px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 12px 15px;
|
||||
background-color: #f3f3f3;
|
||||
border-radius: 4px;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
box-sizing: border-box;
|
||||
|
||||
.attachment-avatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
.attachment-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: 0 0 0 11px;
|
||||
width: calc(100% - 38px - 11px);
|
||||
.attachment-info-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
span {
|
||||
line-height: 20px;
|
||||
color: #191919;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.attachment-sub-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
span {
|
||||
line-height: 17px;
|
||||
color: #999999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.info-detail-searchRecordDetail {
|
||||
display: flex;
|
||||
@ -497,31 +380,4 @@ const previewPDF = (item) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-component-wrapper {
|
||||
width: 154px;
|
||||
height: 100px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.im-message-video,
|
||||
.im-message-image,
|
||||
.image-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.n-image) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep(img),
|
||||
:deep(video) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,16 +16,12 @@ const props = defineProps({
|
||||
createdAt: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
modalTitle: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
})
|
||||
const isShow=defineModel<boolean>('show')
|
||||
const { showUserInfoModal } = useInject()
|
||||
const items = ref<ITalkRecord[]>([])
|
||||
const title = ref(props?.modalTitle || '会话记录')
|
||||
const title = ref('会话记录')
|
||||
|
||||
const onMaskClick = () => {
|
||||
isShow.value=false
|
||||
@ -34,7 +30,7 @@ const onMaskClick = () => {
|
||||
const onLoadData = () => {
|
||||
ServeGetForwardRecords({
|
||||
msg_id: props.msgId,
|
||||
biz_date: parseTime(new Date(props.createdAt || ''), '{y}{m}')
|
||||
biz_date: parseTime(new Date(props.createdAt), '{y}{m}')
|
||||
}).then((res) => {
|
||||
if (res.code == 200) {
|
||||
items.value = res.data.items || []
|
||||
|
@ -12,13 +12,7 @@ const props = defineProps<{
|
||||
const isShowRecord = ref(false)
|
||||
|
||||
const title = computed(() => {
|
||||
const uniqueNames = [...new Set(props.extra.records.map(v => v.nickname))];
|
||||
if (uniqueNames.length <= 2) {
|
||||
return uniqueNames.join('和');
|
||||
} else {
|
||||
return uniqueNames.slice(0, 2).join('和') + '等';
|
||||
}
|
||||
// return [...new Set(props.extra.records.map((v) => v.nickname))].join('和')
|
||||
return [...new Set(props.extra.records.map((v) => v.nickname))].join('、')
|
||||
})
|
||||
|
||||
const onClick = () => {
|
||||
@ -27,7 +21,7 @@ const onClick = () => {
|
||||
</script>
|
||||
<template>
|
||||
<section class="im-message-forward pointer" @click="onClick">
|
||||
<div class="title">{{ extra.forward_name || title}}的会话记录</div>
|
||||
<div class="title">{{ title }} 的会话记录</div>
|
||||
<div class="list" v-for="(record, index) in extra.records" :key="index">
|
||||
<p>
|
||||
<span>{{ record.nickname }}: </span>
|
||||
@ -39,7 +33,7 @@ const onClick = () => {
|
||||
<span>转发:聊天会话记录 ({{ extra.msg_ids.length }}条)</span>
|
||||
</div>
|
||||
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at" :modalTitle="(extra.forward_name || title) + '的会话记录'"/>
|
||||
<ForwardRecord v-model:show="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" :created-at="data.created_at"/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -47,21 +41,19 @@ const onClick = () => {
|
||||
.im-message-forward {
|
||||
width: 250px;
|
||||
min-height: 95px;
|
||||
max-height: 190px;
|
||||
max-height: 150px;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--im-message-border-color);
|
||||
user-select: none;
|
||||
|
||||
.title {
|
||||
max-height: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
@ -11,20 +11,12 @@ defineProps<{
|
||||
let show = ref(false)
|
||||
</script>
|
||||
<template>
|
||||
<section
|
||||
class="im-message-group-notice pointer"
|
||||
@click="show = !show"
|
||||
:class="{
|
||||
left: data.float === 'left',
|
||||
right: data.float === 'right'
|
||||
}"
|
||||
>
|
||||
<section class="im-message-group-notice pointer" @click="show = !show">
|
||||
<div class="title">
|
||||
<!-- <n-tag :bordered="false" size="small" type="primary"> 群公告 </n-tag>
|
||||
《{{ extra.title }}》 -->
|
||||
<text>群公告</text>
|
||||
<n-tag :bordered="false" size="small" type="primary"> 群公告 </n-tag>
|
||||
《{{ extra.title }}》
|
||||
</div>
|
||||
<div class="title" :class="{ ellipsis: !show }">
|
||||
<div class="content" :class="{ ellipsis: !show }">
|
||||
{{ extra.content }}
|
||||
</div>
|
||||
</section>
|
||||
@ -38,14 +30,14 @@ let show = ref(false)
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--im-message-border-color);
|
||||
user-select: none;
|
||||
background-color: #fff;
|
||||
|
||||
.title {
|
||||
line-height: 44rpx;
|
||||
font-size: 32rpx;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
@ -64,18 +56,5 @@ let show = ref(false)
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
background-color: #fff;
|
||||
border-radius: 0 16rpx 16rpx 16rpx;
|
||||
}
|
||||
|
||||
&.right {
|
||||
background-color: #46299d;
|
||||
border-radius: 16rpx 0 16rpx 16rpx;
|
||||
.title {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -27,16 +27,6 @@ const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
revokeInfo: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@ -52,104 +42,16 @@ const onRevoke = () => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="im-message-revoke">
|
||||
<div class="content" v-if="JSON.stringify(revokeInfo) !== '{}'">
|
||||
<span v-if="talk_type === 1 && login_uid === revokeInfo.withdraw_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 1 && login_uid !== revokeInfo.withdraw_id">
|
||||
{{ revokeInfo.withdraw_name }}撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid === revokeInfo.withdraw_id &&
|
||||
login_uid === revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
你撤回了一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid === revokeInfo.withdraw_id &&
|
||||
login_uid !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
你撤回了{{ revokeInfo.retracted_name }}一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
revokeInfo.withdraw_id === revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
login_uid === revokeInfo.retracted_id &&
|
||||
revokeInfo.withdraw_id !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了你一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
talk_type === 2 &&
|
||||
login_uid !== revokeInfo.withdraw_id &&
|
||||
login_uid !== revokeInfo.retracted_id &&
|
||||
revokeInfo.withdraw_id !== revokeInfo.retracted_id
|
||||
"
|
||||
>
|
||||
{{ revokeInfo.withdraw_name }}撤回了{{ revokeInfo.retracted_name }}一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
|
||||
<div style="display: inline-block;" v-if="login_uid === user_id">
|
||||
<n-button
|
||||
@click="onRevoke"
|
||||
v-if="data.msg_type === 1 && data.extra?.content"
|
||||
text
|
||||
class="text-#46299D text-11px"
|
||||
>重新编辑</n-button
|
||||
>
|
||||
<div class="content">
|
||||
<div v-if="login_uid === user_id">
|
||||
<span> 你撤回了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<n-button @click="onRevoke" v-if="data.msg_type === 1&&data.extra?.content&&data.is_self_action" text class="text-#46299D text-11px">重新编辑</n-button>
|
||||
</div>
|
||||
<!-- <span v-if="login_uid == user_idA"> 你撤回B了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<span v-else-if="login_uid == user_idB"> A撤回你了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<span v-else> A撤回B了一条消息 | {{ formatTime(datetime) }} </span> -->
|
||||
</div>
|
||||
<div class="content" v-if="JSON.stringify(revokeInfo) === '{}'">
|
||||
<span v-if="talk_type === 1 && login_uid === user_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
<span v-else-if="talk_type == 1"> 对方撤回了一条消息 | {{ formatTime(datetime) }} </span>
|
||||
<span v-else>
|
||||
"{{ nickname }}" 撤回了一条消息 |
|
||||
{{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 1 && login_uid !== user_id">
|
||||
{{ nickname }}撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 2 && !extra && login_uid === user_id">
|
||||
你撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 2 && !extra && login_uid !== user_id">
|
||||
{{ nickname }}撤回了一条消息 | {{ formatTime(datetime) }}
|
||||
</span>
|
||||
<span v-if="talk_type === 2 && extra"> {{ extra }} | {{ formatTime(datetime) }} </span>
|
||||
<div style="display: inline-block;" v-if="login_uid === user_id">
|
||||
<n-button
|
||||
@click="onRevoke"
|
||||
v-if="data.msg_type === 1 && data.extra?.content"
|
||||
text
|
||||
class="text-#46299D text-11px"
|
||||
>重新编辑</n-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -70,12 +70,6 @@ class Revoke extends Base {
|
||||
useTalkStore().updateItem({
|
||||
index_name: this.getIndexName(),
|
||||
msg_text: this.resource.text,
|
||||
revokeInfo: {
|
||||
retracted_id: this.resource.retracted_id,
|
||||
retracted_name: this.resource.retracted_name,
|
||||
withdraw_id: this.resource.withdraw_id,
|
||||
withdraw_name: this.resource.withdraw_name,
|
||||
},
|
||||
updated_at: parseTime(new Date())
|
||||
})
|
||||
|
||||
@ -86,12 +80,6 @@ class Revoke extends Base {
|
||||
|
||||
useDialogueStore().updateDialogueRecord({
|
||||
msg_id: this.msg_id,
|
||||
revokeInfo: {
|
||||
retracted_id: this.resource.retracted_id,
|
||||
retracted_name: this.resource.retracted_name,
|
||||
withdraw_id: this.resource.withdraw_id,
|
||||
withdraw_name: this.resource.withdraw_name,
|
||||
},
|
||||
is_revoke: 1
|
||||
})
|
||||
}
|
||||
|
@ -51,8 +51,7 @@ export interface ITalkRecord {
|
||||
float: string,
|
||||
is_convert_text?:number//语音记录的 是否是在展示转文本状态 1:是 0:否,
|
||||
erp_user_id:number,
|
||||
read_total_num:number,
|
||||
revokeInfo?: any
|
||||
read_total_num:number
|
||||
}
|
||||
|
||||
export interface ITalkRecordExtraText {
|
||||
@ -82,7 +81,6 @@ export interface ITalkRecordExtraForward {
|
||||
}[]
|
||||
talk_type: number
|
||||
user_id: number
|
||||
forward_name?: any
|
||||
}
|
||||
|
||||
export interface ITalkRecordExtraGroupNotice {
|
||||
|
@ -18,7 +18,7 @@ export function isLoggedIn() {
|
||||
*/
|
||||
export function getAccessToken() {
|
||||
// return storage.get(AccessToken) || ''
|
||||
return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22c9c2f9b60a57573e8b08cdf47105e1ba85550c21fa55526e8a00bf316c623eb67abf749622c48beab908d61d3db7b22ed3eb6aa8a08c77680ad4d8a3458c1e72f97ba2b8480674df77f0501a34e82b58'
|
||||
return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b891a491a664540c3af42964b31bedf8b1c93e8a754bb71e4b95d53ad8e6b16ac1575f536a9e7a062e44f3bb48a367623d38bd875a10afa3a53e79374ffda424138ed9ad4cab0d972432567ae7149b2bf3c'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,6 +68,11 @@ export function clipboard(text, callback) {
|
||||
}
|
||||
|
||||
export async function clipboardImage(src, callback) {
|
||||
// 在wujie环境下使用主应用的clipboard
|
||||
const clipboardObj = window.__POWERED_BY_WUJIE__
|
||||
? window.parent.navigator.clipboard
|
||||
: navigator.clipboard
|
||||
|
||||
const { state } = await navigator.permissions.query({
|
||||
name: 'clipboard-write'
|
||||
})
|
||||
@ -80,7 +85,7 @@ export async function clipboardImage(src, callback) {
|
||||
|
||||
// navigator.clipboard.write 仅支持 png 图片
|
||||
if (blob.type == 'image/png') {
|
||||
await navigator.clipboard.write([
|
||||
await clipboardObj.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob
|
||||
})
|
||||
@ -99,13 +104,13 @@ export async function clipboardImage(src, callback) {
|
||||
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
const data = [new ClipboardItem({ [blob.type]: blob })]
|
||||
|
||||
navigator.clipboard
|
||||
clipboardObj
|
||||
.write(data)
|
||||
.then(() => {
|
||||
callback()
|
||||
|
@ -16,8 +16,6 @@ import avatarModule from '@/components/avatar-module/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const dialogueStore = useDialogueStore()
|
||||
const uploadsStore = useUploadsStore()
|
||||
console.log('dialogueStore', dialogueStore)
|
||||
|
||||
const members = computed(() => dialogueStore.members)
|
||||
const membersByAlphabet = computed(() => {
|
||||
if (state.searchMemberByAlphabet) {
|
||||
|
@ -37,7 +37,6 @@ const labelColor=[
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="nickname">{{ username }}</span>
|
||||
<span v-if="data.talk_type == 2">({{data.group_member_num}})</span>
|
||||
<!-- <span class="badge top" v-show="data.is_top">顶</span>
|
||||
<span class="badge roboot" v-show="data.is_robot">助</span>
|
||||
<span class="badge group" v-show="data.talk_type == 2">群</span> -->
|
||||
|
@ -182,7 +182,7 @@ const onCopyText = (data: ITalkRecord) => {
|
||||
return clipboard(htmlDecode(data.extra.content), () => useMessage.success('复制成功'))
|
||||
}
|
||||
}
|
||||
|
||||
console.log('data.extra?.url',data.extra?.url)
|
||||
if (data.extra?.url) {
|
||||
return clipboardImage(data.extra.url, () => {
|
||||
useMessage.success('复制成功')
|
||||
@ -766,8 +766,6 @@ const onCustomSkipBottomEvent = () => {
|
||||
:nickname="item.nickname"
|
||||
:talk_type="item.talk_type"
|
||||
:datetime="item.created_at"
|
||||
:revokeInfo="item.revokeInfo"
|
||||
:extra="item.extra"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -147,7 +147,6 @@ const onSetMenu = () => {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
@ -33,6 +33,7 @@ export function useMenu() {
|
||||
const showDropdownMenu = (e: any, uid: number, item: any) => {
|
||||
// dropdown.item = Object.assign({}, item)
|
||||
dropdown.item = item
|
||||
dropdown.item.is_self_action = true
|
||||
dropdown.options = []
|
||||
if ([4].includes(item.msg_type)) {
|
||||
if(item.is_convert_text === 1){
|
||||
@ -48,9 +49,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' })
|
||||
|
||||
|
||||
|
@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
|
||||
vueJsx({}),
|
||||
compressPlugin(),
|
||||
UnoCSS(),
|
||||
vueDevTools({
|
||||
launchEditor: 'trae',
|
||||
})
|
||||
// vueDevTools({
|
||||
// launchEditor: 'trae',
|
||||
// })
|
||||
],
|
||||
define: {
|
||||
__APP_ENV__: env.APP_ENV
|
||||
|
Loading…
Reference in New Issue
Block a user