Compare commits

..

23 Commits
main ... xingyy

Author SHA1 Message Date
Phoenix
4b5c160e94 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-10 15:03:31 +08:00
Phoenix
ebd567a757 fix(消息面板): 修复消息菜单和撤回消息按钮的显示逻辑
修复消息菜单中缺少的is_self_action属性设置,确保撤回消息按钮仅在自身操作时显示
调整编辑器内容处理逻辑,优化草稿保存的数据结构
2025-06-10 15:03:29 +08:00
18871db6b6 Merge branch 'main' into dev 2025-06-10 14:56:10 +08:00
Phoenix
1ae317dbb3 Merge branch 'xingyy' into dev 2025-06-10 13:39:29 +08:00
Phoenix
e4354d42cd feat(消息面板): 添加dayjs依赖并优化消息撤回时间计算
使用dayjs替换原有的日期处理逻辑,提高代码可读性并延长消息撤回时间至5分钟
2025-06-10 13:28:54 +08:00
Phoenix
8bba2d64af fix(editor): 优化提及插入逻辑并修复光标位置问题
重构提及插入逻辑,使用更直接的方式删除@符号到光标间的内容
将普通空格替换为不间断空格以避免被HTML压缩
确保光标始终正确放置在插入内容之后
2025-06-10 11:03:24 +08:00
Phoenix
d4e52152ef feat(editor): 添加鼠标点击选择mention功能并优化插入逻辑
- 新增handleMentionSelectByMouse函数处理鼠标点击选择mention
- 重构insertMention函数,支持传入range参数并优化插入逻辑
- 修复mention列表点击事件,防止默认行为导致的问题
- 优化onSubscribeMention函数,确保焦点和选区正确处理
2025-06-10 09:45:10 +08:00
Phoenix
bdf07155c8 fix(editor): 修复提及功能中用户ID处理问题
修复提及成员时用户ID类型转换问题,确保ID统一为字符串类型。同时为管理员添加"全体成员"提及选项,并完善提及列表的数据处理逻辑。
2025-06-09 16:48:52 +08:00
Phoenix
b905db0cfa fix: 优化消息撤回逻辑和编辑器内容处理
- 调整消息菜单的撤回选项显示逻辑,区分单聊和群聊场景
- 修复编辑器内容处理,使用trimEnd替代trim避免尾部空格问题
- 移除重复的quote元素删除操作
- 优化编辑器空内容判断逻辑
2025-06-09 15:29:24 +08:00
Phoenix
3b6d998ce1 Merge branch 'xingyy' into dev 2025-06-09 14:46:59 +08:00
Phoenix
5340461a7e fix(utils): 修复wujie环境下剪贴板功能兼容性问题
修改clipboardImage方法以支持wujie微前端环境,使用主应用的navigator.clipboard对象
同时优化canvas图片绘制参数,确保图片缩放正确
2025-06-09 14:46:33 +08:00
Phoenix
45eec2ff22 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-09 13:57:38 +08:00
Phoenix
9c34066128 Merge branch 'xingyy' into dev 2025-06-09 13:57:32 +08:00
Phoenix
628894a254 refactor(editor): 优化mention处理逻辑并移除调试日志
移除调试用的console.log语句
重构mention列表过滤逻辑,使用startsWith替代includes
添加Backspace和Delete键删除mention元素的功能
优化键盘事件处理逻辑,减少不必要的DOM操作
2025-06-09 13:57:15 +08:00
92fce58429 Merge branch 'main' into dev 2025-06-09 13:33:02 +08:00
Phoenix
2e998a1174 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-09 11:51:48 +08:00
Phoenix
60a2fb996b Merge branch 'xingyy' into dev 2025-06-09 11:51:38 +08:00
b282562cdd Merge branch 'main' into dev 2025-06-06 18:54:59 +08:00
d0abf7d8ab Merge branch 'main' into dev 2025-06-06 09:05:56 +08:00
Phoenix
409af72039 Merge branch 'dev' of http://172.16.100.91:3000/scout666/chat-pc into dev 2025-06-05 14:14:47 +08:00
Phoenix
799599bd83 Merge branch 'xingyy' into dev 2025-06-05 14:14:38 +08:00
ec18d85546 Merge branch 'wyfMain-dev' into dev 2025-06-05 09:20:27 +08:00
Phoenix
a97f293a6c Merge branch 'xingyy' into dev
# Conflicts:
#	src/views/message/inner/panel/PanelFooter.vue   resolved by xingyy version
2025-06-04 16:32:24 +08:00
19 changed files with 393 additions and 468 deletions

View File

@ -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",

View File

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

View File

@ -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
}
// BackspaceDeletemention
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') {
// Backspacemention
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') {
// Deletemention
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+EnterShift+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">

View File

@ -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) {

View File

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

View File

@ -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 || []

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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'
}
/**

View File

@ -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()

View File

@ -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) {

View File

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

View File

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

View File

@ -147,7 +147,6 @@ const onSetMenu = () => {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0 5px 0 0;
}
}

View File

@ -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' })

View File

@ -46,9 +46,9 @@ export default defineConfig(({ mode }) => {
vueJsx({}),
compressPlugin(),
UnoCSS(),
vueDevTools({
launchEditor: 'trae',
})
// vueDevTools({
// launchEditor: 'trae',
// })
],
define: {
__APP_ENV__: env.APP_ENV