Compare commits
	
		
			No commits in common. "f511244d5996ebac51d5c0f83374367ffa5a2212" and "928c4f91f95a1b7ec4e9fd66ec6e7e0c32be7485" have entirely different histories.
		
	
	
		
			f511244d59
			...
			928c4f91f9
		
	
		
							
								
								
									
										1
									
								
								env/.env.test
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								env/.env.test
									
									
									
									
										vendored
									
									
								
							| @ -8,5 +8,4 @@ VUE_APP_PREVIEW=false | |||||||
|  VITE_BASE_API=http://114.218.158.24:8503 |  VITE_BASE_API=http://114.218.158.24:8503 | ||||||
|  VITE_SOCKET_API=ws://114.218.158.24:8504 |  VITE_SOCKET_API=ws://114.218.158.24:8504 | ||||||
| VITE_EPR_BASEURL=http://114.218.158.24:9020 | VITE_EPR_BASEURL=http://114.218.158.24:9020 | ||||||
| VITE_PAGE_URL=http://172.16.100.93:9032 |  | ||||||
| VUE_APP_WEBSITE_NAME="" | VUE_APP_WEBSITE_NAME="" | ||||||
| @ -437,7 +437,8 @@ function onClipboardMatcher(node: any, Delta) { | |||||||
| function onSendMessage() { | function onSendMessage() { | ||||||
|   var delta = getQuill().getContents() |   var delta = getQuill().getContents() | ||||||
|   let data = deltaToMessage(delta)  // 转换Delta为消息格式 |   let data = deltaToMessage(delta)  // 转换Delta为消息格式 | ||||||
|   if (data.items.length === 0||!data.items[0].content.trim()) { | 
 | ||||||
|  |   if (data.items.length === 0) { | ||||||
|     return  // 没有内容不发送 |     return  // 没有内容不发送 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -88,7 +88,7 @@ const strokeDashoffset = computed(() => | |||||||
| const handleClick = () => { | const handleClick = () => { | ||||||
|   if(!props.extra.is_uploading){ |   if(!props.extra.is_uploading){ | ||||||
|     window.open( |     window.open( | ||||||
|     `${import.meta.env.VITE_PAGE_URL}/office?url=${props.extra.path}`, |     `${window.location.origin}/office?url=${props.extra.path}`, | ||||||
|     '_blank', |     '_blank', | ||||||
|     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' |     'width=1200,height=900,left=200,top=200,toolbar=no,menubar=no,scrollbars=yes,resizable=yes,location=no,status=no' | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -21,7 +21,7 @@ export function useFriendsMenu() { | |||||||
|     dropdown.item = Object.assign({}, item) |     dropdown.item = Object.assign({}, item) | ||||||
| 
 | 
 | ||||||
|     dropdown.options = [] |     dropdown.options = [] | ||||||
|     // dropdown.options.push({ label: '删除好友', key: 'delete' })
 |     dropdown.options.push({ label: '删除好友', key: 'delete' }) | ||||||
| 
 | 
 | ||||||
|     dropdown.x = e.clientX |     dropdown.x = e.clientX | ||||||
|     dropdown.y = e.clientY |     dropdown.y = e.clientY | ||||||
|  | |||||||
| @ -71,11 +71,11 @@ export function useSessionMenu() { | |||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     if (item.talk_type == 1) { |     if (item.talk_type == 1) { | ||||||
|       // options.push({
 |       options.push({ | ||||||
|   |   | ||||||
|       //   label: '删除好友',
 |         label: '删除好友', | ||||||
|       //   key: 'delete_contact'
 |         key: 'delete_contact' | ||||||
|       // })
 |       }) | ||||||
|     } else { |     } else { | ||||||
|       options.push({ |       options.push({ | ||||||
|        |        | ||||||
| @ -107,7 +107,9 @@ export function useSessionMenu() { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const onUserInfo = (item: ISession) => { |   const onUserInfo = (item: ISession) => { | ||||||
|     user(item.erp_user_id) |     console.error('item',item) | ||||||
|  | 
 | ||||||
|  |     user(item.id) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // 移除会话
 |   // 移除会话
 | ||||||
|  | |||||||
| @ -27,7 +27,6 @@ export interface ISession { | |||||||
|   content?: string // 消息内容
 |   content?: string // 消息内容
 | ||||||
|   last_message?: ISessionLastMessage |   last_message?: ISessionLastMessage | ||||||
|   draft_text?: string // 草稿文本
 |   draft_text?: string // 草稿文本
 | ||||||
|   erp_user_id:number |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 消息记录
 | // 消息记录
 | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ export function isLoggedIn() { | |||||||
|  */ |  */ | ||||||
| export function getAccessToken() { | export function getAccessToken() { | ||||||
|   // return storage.get(AccessToken) || ''
 |   // return storage.get(AccessToken) || ''
 | ||||||
|   return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22726726265e9af0db370a54ea5ee002b43662d571b84c8468ac15330f79503a5cd5e72282d8bee92749b1a3c1b7fd87ae70b64b90e437e84c1b558c64a35e181b2ecf5db3007680c3607eac1edee7f59d' |   return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22a02b52442e2e6a79ca35b2fa54a4bb215abd3b9cc8ad2ef018b380a88e93a2fc55fb3e36695ae76a0ea52c973374861c70339249f89f36d787b0dfa7fc99339470dbc626eb5ff42232137ef97447e79f' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { watch, onMounted, ref, nextTick } from 'vue' | import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue' | ||||||
| import { NDropdown, NCheckbox } from 'naive-ui' | import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui' | ||||||
| import { Loading, MoreThree, ToTop } from '@icon-park/vue-next' | import { Loading, MoreThree, ToTop } from '@icon-park/vue-next' | ||||||
| import { bus } from '@/utils/event-bus' | import { bus } from '@/utils/event-bus' | ||||||
| import { useDialogueStore } from '@/store' | import { useDialogueStore } from '@/store' | ||||||
| @ -16,8 +16,40 @@ import { useInject, useTalkRecord, useUtil } from '@/hooks' | |||||||
| import { ExclamationCircleFilled } from '@ant-design/icons-vue' | import { ExclamationCircleFilled } from '@ant-design/icons-vue' | ||||||
| import { useUserStore, useUploadsStore } from '@/store' | import { useUserStore, useUploadsStore } from '@/store' | ||||||
| import RevokeMessage from '@/components/talk/message/RevokeMessage.vue' | import RevokeMessage from '@/components/talk/message/RevokeMessage.vue' | ||||||
| import { voiceToText } from '@/api/chat.js' | import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js' | ||||||
| import { confirmBox } from '@/components/confirm-box/service.js' | import { confirmBox } from '@/components/confirm-box/service.js' | ||||||
|  | import ws from '@/connect' | ||||||
|  | import avatarModule from '@/components/avatar-module/index.vue' | ||||||
|  | 
 | ||||||
|  | // 定义消息已读状态接口 | ||||||
|  | interface ReadStatus { | ||||||
|  |   msg_ids: string[] | ||||||
|  |   talk_type: number | ||||||
|  |   receiver_id: number | ||||||
|  |   user_id?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 定义状态接口 | ||||||
|  | interface State { | ||||||
|  |   visibleElements: Set<HTMLElement> | ||||||
|  |   visibleOutElements: Set<HTMLElement> | ||||||
|  |   tempWaitDoRead: ReadStatus[] | ||||||
|  |   tempWaitDoCheck: ReadStatus[] | ||||||
|  |   setMessageReadInterval: number | null | ||||||
|  |   setOutMessageReadInterval: number | null | ||||||
|  |   lastUpdateTime: number | ||||||
|  |   isScrolling: boolean | ||||||
|  |   scrollTimer: number | null | ||||||
|  |   lastTriggerTime: number | ||||||
|  |   talkReadListDetail: any[] | ||||||
|  |   readDetailIsUnread: number | ||||||
|  |   currentMsgReadDetail: any | null | ||||||
|  |   currentReadDetailPage: number | ||||||
|  |   hasMoreReadListDetail: boolean | ||||||
|  |   loadingReadListDetail: boolean | ||||||
|  |   lastScrollTop: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   uid: { |   uid: { | ||||||
|     type: Number, |     type: Number, | ||||||
| @ -38,10 +70,14 @@ const props = defineProps({ | |||||||
|   specifiedMsg: { |   specifiedMsg: { | ||||||
|     type: String, |     type: String, | ||||||
|     default: '' |     default: '' | ||||||
|  |   }, | ||||||
|  |   num: { | ||||||
|  |     type: Number, | ||||||
|  |     default: 0 | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid) | const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid) | ||||||
| const uploadsStore = useUploadsStore() | const uploadsStore = useUploadsStore() | ||||||
| const { useMessage } = useUtil() | const { useMessage } = useUtil() | ||||||
| const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu() | const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu() | ||||||
| @ -90,11 +126,33 @@ const onPanelScroll = (e: any) => { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const height = e.target.scrollTop + e.target.clientHeight |   const height = e.target.scrollTop + e.target.clientHeight | ||||||
|  |   const scrollHeight = e.target.scrollHeight | ||||||
|  |   const isScrollingDown = e.target.scrollTop > state.value.lastScrollTop | ||||||
|  |   state.value.lastScrollTop = e.target.scrollTop | ||||||
| 
 | 
 | ||||||
|   skipBottom.value = height < e.target.scrollHeight - 200 |   // 触底且必须是向下滚动 | ||||||
|  |   if (height === scrollHeight && isScrollingDown) { | ||||||
|  |     onLoadMoreDown() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   skipBottom.value = height < scrollHeight - 200 | ||||||
|   if (!skipBottom.value && dialogueStore.unreadBubble) { |   if (!skipBottom.value && dialogueStore.unreadBubble) { | ||||||
|     dialogueStore.setUnreadBubble(0) |     dialogueStore.setUnreadBubble(0) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // 设置滚动状态 | ||||||
|  |   state.value.isScrolling = true | ||||||
|  |   if (state.value.scrollTimer) { | ||||||
|  |     clearTimeout(state.value.scrollTimer) | ||||||
|  |   } | ||||||
|  |   state.value.scrollTimer = window.setTimeout(() => { | ||||||
|  |     state.value.isScrolling = false | ||||||
|  |     // 滚动停止,强制触发一次 | ||||||
|  |     checkVisibleOutElements() | ||||||
|  |     // 重置节流时间戳,保证下一次变化能立刻触发 | ||||||
|  |     lastVisibleOutTriggerTime = Date.now() | ||||||
|  |   }, 300) // 300ms 的防抖时间 | ||||||
|  | 
 | ||||||
|   // 检测是否到达底部 |   // 检测是否到达底部 | ||||||
|   if (skipBottom.value == false) { |   if (skipBottom.value == false) { | ||||||
|     let len = dialogueStore.records.length |     let len = dialogueStore.records.length | ||||||
| @ -231,7 +289,6 @@ const onClickNickname = (data: ITalkRecord) => { | |||||||
| 
 | 
 | ||||||
| // 会话列表右键显示菜单 | // 会话列表右键显示菜单 | ||||||
| const onContextMenu = (e: any, item: ITalkRecord) => { | const onContextMenu = (e: any, item: ITalkRecord) => { | ||||||
| 
 |  | ||||||
|   if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) { |   if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) { | ||||||
|     return e.preventDefault() |     return e.preventDefault() | ||||||
|   } |   } | ||||||
| @ -294,7 +351,10 @@ watch( | |||||||
|       try { |       try { | ||||||
|         const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg)) |         const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg)) | ||||||
|         // 只有会话id和参数都匹配才进入特殊模式 |         // 只有会话id和参数都匹配才进入特殊模式 | ||||||
|         if (parsed.talk_type === newProps.talk_type && parsed.receiver_id === newProps.receiver_id) { |         if ( | ||||||
|  |           parsed.talk_type === newProps.talk_type && | ||||||
|  |           parsed.receiver_id === newProps.receiver_id | ||||||
|  |         ) { | ||||||
|           specialParams = parsed |           specialParams = parsed | ||||||
|         } |         } | ||||||
|       } catch (e) {} |       } catch (e) {} | ||||||
| @ -323,7 +383,6 @@ const retry=(item:any)=>{ | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const onContextMenuAvatar = (e: any, item: any) => { | const onContextMenuAvatar = (e: any, item: any) => { | ||||||
|   if(item.talk_type!==1){ |  | ||||||
|   e.preventDefault() |   e.preventDefault() | ||||||
|   if (item.float !== 'right') { |   if (item.float !== 'right') { | ||||||
|     bus.emit(EditorConst.Mention, { |     bus.emit(EditorConst.Mention, { | ||||||
| @ -333,8 +392,330 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const state = ref<State>({ | ||||||
|  |   visibleElements: new Set(), | ||||||
|  |   visibleOutElements: new Set(), | ||||||
|  |   tempWaitDoRead: [], | ||||||
|  |   tempWaitDoCheck: [], | ||||||
|  |   setMessageReadInterval: null, | ||||||
|  |   setOutMessageReadInterval: null, | ||||||
|  |   lastUpdateTime: 0, | ||||||
|  |   isScrolling: false, | ||||||
|  |   scrollTimer: null, | ||||||
|  |   lastTriggerTime: 0, | ||||||
|  |   talkReadListDetail: [], | ||||||
|  |   readDetailIsUnread: 1, | ||||||
|  |   currentMsgReadDetail: null, | ||||||
|  |   currentReadDetailPage: 1, | ||||||
|  |   hasMoreReadListDetail: true, | ||||||
|  |   loadingReadListDetail: false, | ||||||
|  |   lastScrollTop: 0 | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 定义观察者变量 | ||||||
|  | let observer: IntersectionObserver | null = null | ||||||
|  | 
 | ||||||
|  | // 检查需要发送已读回执的元素 | ||||||
|  | const checkVisibleElements = () => { | ||||||
|  |   if (state.value.visibleElements.size > 0) { | ||||||
|  |     let waitDoRead: ReadStatus[] = [] | ||||||
|  |     state.value.visibleElements.forEach((el: HTMLElement) => { | ||||||
|  |       const msgId = el.dataset.msgid | ||||||
|  |       const talkType = Number(el.dataset.talktype) | ||||||
|  |       const receiverId = Number(el.dataset.receiverid) | ||||||
|  | 
 | ||||||
|  |       if (!msgId) return | ||||||
|  | 
 | ||||||
|  |       if (waitDoRead.length === 0) { | ||||||
|  |         waitDoRead.push({ | ||||||
|  |           msg_ids: [msgId], | ||||||
|  |           talk_type: talkType, | ||||||
|  |           receiver_id: receiverId | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|  |         const existingItem = waitDoRead.find( | ||||||
|  |           (item) => item.talk_type === talkType && item.receiver_id === receiverId | ||||||
|  |         ) | ||||||
|  |         if (existingItem) { | ||||||
|  |           existingItem.msg_ids.push(msgId) | ||||||
|  |         } else { | ||||||
|  |           waitDoRead.push({ | ||||||
|  |             msg_ids: [msgId], | ||||||
|  |             talk_type: talkType, | ||||||
|  |             receiver_id: receiverId | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     if (waitDoRead.length > 0) { | ||||||
|  |       waitDoRead.forEach((doReadItem) => { | ||||||
|  |         const prevItem = state.value.tempWaitDoRead.find( | ||||||
|  |           (prev) => | ||||||
|  |             prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id | ||||||
|  |         ) | ||||||
|  |         if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) { | ||||||
|  |           console.error('====发送了新版已读回执=====', doReadItem) | ||||||
|  |           ws.emit('im.message.new.read', doReadItem) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |     state.value.tempWaitDoRead = JSON.parse(JSON.stringify(waitDoRead)) | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // 检查需要获取别人发送的已读回执列表的元素 | ||||||
|  | const checkVisibleOutElements = () => { | ||||||
|  |   if (state.value.visibleOutElements.size > 0) { | ||||||
|  |     let waitDoCheck: ReadStatus[] = [] | ||||||
|  |     state.value.visibleOutElements.forEach((el: HTMLElement) => { | ||||||
|  |       const msgId = el.dataset.msgid | ||||||
|  |       const talkType = Number(el.dataset.talktype) | ||||||
|  |       const receiverId = Number(el.dataset.receiverid) | ||||||
|  | 
 | ||||||
|  |       if (!msgId) return | ||||||
|  | 
 | ||||||
|  |       if (waitDoCheck.length === 0) { | ||||||
|  |         waitDoCheck.push({ | ||||||
|  |           msg_ids: [msgId], | ||||||
|  |           talk_type: talkType, | ||||||
|  |           receiver_id: receiverId, | ||||||
|  |           user_id: props.uid | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|  |         const existingItem = waitDoCheck.find( | ||||||
|  |           (item) => item.talk_type === talkType && item.receiver_id === receiverId | ||||||
|  |         ) | ||||||
|  |         if (existingItem) { | ||||||
|  |           existingItem.msg_ids.push(msgId) | ||||||
|  |         } else { | ||||||
|  |           waitDoCheck.push({ | ||||||
|  |             msg_ids: [msgId], | ||||||
|  |             talk_type: talkType, | ||||||
|  |             receiver_id: receiverId, | ||||||
|  |             user_id: props.uid | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     if (waitDoCheck.length > 0) { | ||||||
|  |       waitDoCheck.forEach((doCheckItem) => { | ||||||
|  |         console.error('====组装了新版已读回执参数,需要发送socket=====', doCheckItem) | ||||||
|  |         ws.emit('im.message.listen.read', doCheckItem) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |     state.value.tempWaitDoCheck = JSON.parse(JSON.stringify(waitDoCheck)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 定义节流时间戳 | ||||||
|  | let lastVisibleOutTriggerTime = 0 | ||||||
|  | 
 | ||||||
|  | //新版采用socket监听已读回执,不轮询接口 | ||||||
|  | watch( | ||||||
|  |   () => state.value.visibleOutElements, | ||||||
|  |   (newVal) => { | ||||||
|  |     const now = Date.now() | ||||||
|  |     if (now - lastVisibleOutTriggerTime < 1000) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     lastVisibleOutTriggerTime = now | ||||||
|  |     checkVisibleOutElements() | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     deep: true, | ||||||
|  |     immediate: true | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 观察者函数 | ||||||
|  | const handleIntersection = (entries) => { | ||||||
|  |   entries.forEach((entry) => { | ||||||
|  |     if (entry.isIntersecting && entry.intersectionRatio >= 0.5) { | ||||||
|  |       let elData = entry.target.dataset | ||||||
|  |       const msgType = elData.msgtype | ||||||
|  |       const userId = elData.userid | ||||||
|  |       if (Number(msgType) < 1000 && Number(userId) !== Number(props.uid)) { | ||||||
|  |         //我读别人发的消息,需要发送已读回执 | ||||||
|  |         state.value.visibleElements.add(entry.target) | ||||||
|  |       } | ||||||
|  |       if (Number(msgType) < 1000 && Number(userId) === Number(props.uid)) { | ||||||
|  |         //我发的消息,需要获取别人发送的已读回执列表 | ||||||
|  |         state.value.visibleOutElements.add(entry.target) | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // 元素离开视口,从集合中移除 | ||||||
|  |       state.value.visibleElements.delete(entry.target) | ||||||
|  |       state.value.visibleOutElements.delete(entry.target) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听消息列表变化 | ||||||
|  | watch( | ||||||
|  |   () => records.value, | ||||||
|  |   () => { | ||||||
|  |     nextTick(() => { | ||||||
|  |       // 断开旧的观察者 | ||||||
|  |       if (observer) { | ||||||
|  |         observer.disconnect() | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // 重新初始化观察者 | ||||||
|  |       const options = { | ||||||
|  |         root: null, | ||||||
|  |         threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], | ||||||
|  |         rootMargin: '50px 0px' | ||||||
|  |       } | ||||||
|  |       observer = new IntersectionObserver(handleIntersection, options) | ||||||
|  | 
 | ||||||
|  |       // 重新观察所有消息元素 | ||||||
|  |       const messageElements = document.querySelectorAll('.message-item') | ||||||
|  |       messageElements.forEach((el) => { | ||||||
|  |         if (observer) { | ||||||
|  |           observer.observe(el) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }, | ||||||
|  |   { deep: true } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 事件总线防抖处理 | ||||||
|  | let eventBusDebounceTimer: number | null = null | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  |   // 事件总线监听 | ||||||
|  |   bus.subscribe('check-visible-out-elements', (type) => { | ||||||
|  |     if (eventBusDebounceTimer) { | ||||||
|  |       clearTimeout(eventBusDebounceTimer) | ||||||
|  |     } | ||||||
|  |     eventBusDebounceTimer = window.setTimeout(() => { | ||||||
|  |       checkVisibleOutElements() | ||||||
|  |       eventBusDebounceTimer = null | ||||||
|  |     }, 500) | ||||||
|  |   }) | ||||||
|  |   //设置观察者前设置定时器 | ||||||
|  |   if (state.value.setMessageReadInterval) { | ||||||
|  |     clearInterval(state.value.setMessageReadInterval) | ||||||
|  |     state.value.setMessageReadInterval = null | ||||||
|  |   } | ||||||
|  |   state.value.setMessageReadInterval = setInterval(() => { | ||||||
|  |     checkVisibleElements() | ||||||
|  |   }, 2000) | ||||||
|  | 
 | ||||||
|  |   if (state.value.setOutMessageReadInterval) { | ||||||
|  |     clearInterval(state.value.setOutMessageReadInterval) | ||||||
|  |     state.value.setOutMessageReadInterval = null | ||||||
|  |   } | ||||||
|  |   // 旧版采用定时器来轮询已读回执,新版采用socket监听已读回执 | ||||||
|  |   // state.value.setOutMessageReadInterval = setInterval(() => { | ||||||
|  |   //   checkVisibleOutElements() | ||||||
|  |   // }, 2000) | ||||||
|  | 
 | ||||||
|  |   //初始化设置观察者 | ||||||
|  |   const options = { | ||||||
|  |     root: null, | ||||||
|  |     threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], | ||||||
|  |     rootMargin: '50px 0px' | ||||||
|  |   } | ||||||
|  |   observer = new IntersectionObserver(handleIntersection, options) | ||||||
|  | 
 | ||||||
|  |   // 观察所有消息元素 | ||||||
|  |   nextTick(() => { | ||||||
|  |     const messageElements = document.querySelectorAll('.message-item') | ||||||
|  |     messageElements.forEach((el) => { | ||||||
|  |       if (observer) { | ||||||
|  |         observer.observe(el) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | onUnmounted(() => { | ||||||
|  |   if (observer) { | ||||||
|  |     observer.disconnect() | ||||||
|  |   } | ||||||
|  |   if (state.value.setMessageReadInterval) { | ||||||
|  |     clearInterval(state.value.setMessageReadInterval) | ||||||
|  |     state.value.setMessageReadInterval = null | ||||||
|  |     checkVisibleElements() | ||||||
|  |   } | ||||||
|  |   if (state.value.setOutMessageReadInterval) { | ||||||
|  |     clearInterval(state.value.setOutMessageReadInterval) | ||||||
|  |     state.value.setOutMessageReadInterval = null | ||||||
|  |     checkVisibleOutElements() | ||||||
|  |   } | ||||||
|  |   // 清理事件总线防抖定时器 | ||||||
|  |   if (eventBusDebounceTimer) { | ||||||
|  |     clearTimeout(eventBusDebounceTimer) | ||||||
|  |     eventBusDebounceTimer = null | ||||||
|  |   } | ||||||
|  |   // 事件总线移除监听 | ||||||
|  |   bus.unsubscribe('check-visible-out-elements', checkVisibleOutElements) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | //点击显示对应消息的已读回执详情 | ||||||
|  | const toShowMessageReadDetail = async (item?: ITalkRecord) => { | ||||||
|  |   if (item) { | ||||||
|  |     state.value.currentMsgReadDetail = item | ||||||
|  |     onReadTabChange('unread-tab') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   let params = { | ||||||
|  |     page: state.value.currentReadDetailPage, | ||||||
|  |     pageSize: 10, | ||||||
|  |     type: 'detail', //list是列表,detail是详情 | ||||||
|  |     talkType: state?.value?.currentMsgReadDetail?.talk_type, //1私聊2群聊 | ||||||
|  |     receiverId: state?.value?.currentMsgReadDetail?.receiver_id, //私聊的时候是对方用户id,群聊的时候是对方群id | ||||||
|  |     msgId: state?.value?.currentMsgReadDetail?.msg_id, | ||||||
|  |     isUnread: state?.value?.readDetailIsUnread //不送或者送0代表看已读,送1看未读 | ||||||
|  |   } | ||||||
|  |   console.log(params) | ||||||
|  |   const resp = await ServeMessageReadDetail(params) | ||||||
|  |   console.log(resp) | ||||||
|  |   if (resp.code === 200) { | ||||||
|  |     console.log(resp?.data?.data?.length) | ||||||
|  |     if (resp?.data?.data?.length > 0) { | ||||||
|  |       state.value.hasMoreReadListDetail = true | ||||||
|  |       if (state.value.currentReadDetailPage === 1) { | ||||||
|  |         state.value.talkReadListDetail = resp.data.data | ||||||
|  |       } else { | ||||||
|  |         state.value.talkReadListDetail = [...state.value.talkReadListDetail, ...resp.data.data] | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (state.value.currentReadDetailPage === 1) { | ||||||
|  |         state.value.talkReadListDetail = [] | ||||||
|  |       } | ||||||
|  |       state.value.hasMoreReadListDetail = false | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //已读回执tab切换 | ||||||
|  | const onReadTabChange = (value) => { | ||||||
|  |   if (value === 'unread-tab') { | ||||||
|  |     //未读 | ||||||
|  |     state.value.readDetailIsUnread = 1 | ||||||
|  |   } else if (value === 'read-tab') { | ||||||
|  |     //已读 | ||||||
|  |     state.value.readDetailIsUnread = 0 | ||||||
|  |   } | ||||||
|  |   state.value.currentReadDetailPage = 1 | ||||||
|  |   toShowMessageReadDetail() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //加载更多已读回执 | ||||||
|  | const loadMoreReadListDetail = () => { | ||||||
|  |   console.log('loadMoreReadListDetail') | ||||||
|  |   if (!state.value.hasMoreReadListDetail || state.value.loadingReadListDetail) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   state.value.loadingReadListDetail = true | ||||||
|  |   state.value.currentReadDetailPage++ | ||||||
|  |   toShowMessageReadDetail().finally(() => { | ||||||
|  |     state.value.loadingReadListDetail = false | ||||||
|  |   }) | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -356,6 +737,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|         v-for="(item, index) in records" |         v-for="(item, index) in records" | ||||||
|         :key="item.msg_id" |         :key="item.msg_id" | ||||||
|         :id="item.msg_id" |         :id="item.msg_id" | ||||||
|  |         :data-msgid="item.msg_id" | ||||||
|  |         :data-msgtype="item.msg_type" | ||||||
|  |         :data-userid="item.user_id" | ||||||
|  |         :data-talktype="props?.talk_type" | ||||||
|  |         :data-receiverid="props?.receiver_id" | ||||||
|       > |       > | ||||||
|         <!-- 系统消息 --> |         <!-- 系统消息 --> | ||||||
|         <div v-if="item.msg_type >= 1000" class="message-box"> |         <div v-if="item.msg_type >= 1000" class="message-box"> | ||||||
| @ -389,7 +775,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|         > |         > | ||||||
|           <!-- 多选按钮 --> |           <!-- 多选按钮 --> | ||||||
|           <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0"> |           <aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0"> | ||||||
|             <n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" /> |             <n-checkbox | ||||||
|  |               size="small" | ||||||
|  |               :checked="item.isCheck" | ||||||
|  |               @update:checked="item.isCheck = !item.isCheck" | ||||||
|  |             /> | ||||||
|           </aside> |           </aside> | ||||||
|           <!-- 头像信息 --> |           <!-- 头像信息 --> | ||||||
| 
 | 
 | ||||||
| @ -417,9 +807,7 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|               <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> |               <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> | ||||||
|             </div> --> |             </div> --> | ||||||
|             <div class="talk-title"> |             <div class="talk-title"> | ||||||
|               <span class="mr-7px" |               <span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'" | ||||||
|                 v-show="talk_type == 2 && item.float == 'left'" |  | ||||||
|                  |  | ||||||
|                 >{{ item.nickname }} |                 >{{ item.nickname }} | ||||||
|               </span> |               </span> | ||||||
|               <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> |               <span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span> | ||||||
| @ -479,6 +867,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|                 {{ item.extra?.reply?.content }} |                 {{ item.extra?.reply?.content }} | ||||||
|               </span> |               </span> | ||||||
|             </div> |             </div> | ||||||
|  | 
 | ||||||
|  |             <!-- 已读回执 --> | ||||||
|  |             <div class="talk_read_num" v-if="item.user_id === props.uid"> | ||||||
|  |               <span v-if="props.talk_type === 1">{{ | ||||||
|  |                     item.read_total_num > 0 ? '已读' : '未读' | ||||||
|  |                   }}</span> | ||||||
|  |               <n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;" v-if="props.talk_type === 2"> | ||||||
|  |                 <template #trigger> | ||||||
|  |                   <span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)" style="cursor: pointer;"> | ||||||
|  |                     已读 ({{ item?.read_total_num || 0 }}/{{ | ||||||
|  |                       props.num - 1 > 0 ? props.num - 1 : 0 | ||||||
|  |                     }}) | ||||||
|  |                   </span> | ||||||
|  |                 </template> | ||||||
|  |                 <div class="talk-read-list-detail"> | ||||||
|  |                   <n-tabs | ||||||
|  |                     type="line" | ||||||
|  |                     animated | ||||||
|  |                     justify-content="space-around" | ||||||
|  |                     @update:value="onReadTabChange" | ||||||
|  |                   > | ||||||
|  |                     <n-tab name="unread-tab"> | ||||||
|  |                       {{ `未读(${props.num - 1 - (item.read_total_num || 0) || 0})` }} | ||||||
|  |                     </n-tab> | ||||||
|  |                     <n-tab name="read-tab"> | ||||||
|  |                       {{ `已读(${item.read_total_num || 0})` }} | ||||||
|  |                     </n-tab> | ||||||
|  |                   </n-tabs> | ||||||
|  |                   <div class="talk-read-list"> | ||||||
|  |                     <n-infinite-scroll style="height: 340px;" @load="loadMoreReadListDetail"> | ||||||
|  |                       <div | ||||||
|  |                         class="talk-read-list-item" | ||||||
|  |                         v-for="(talkReadDetailItem, | ||||||
|  |                         talkReadDetailIndex) in state.talkReadListDetail" | ||||||
|  |                         :key="talkReadDetailIndex" | ||||||
|  |                       > | ||||||
|  |                         <avatarModule | ||||||
|  |                           :mode="1" | ||||||
|  |                           :avatar="talkReadDetailItem.avatar" | ||||||
|  |                           :userName="talkReadDetailItem.nickName" | ||||||
|  |                           :groupType="0" | ||||||
|  |                           :customStyle="{ | ||||||
|  |                             width: '36px', | ||||||
|  |                             height: '36px' | ||||||
|  |                           }" | ||||||
|  |                           :customTextStyle="{ | ||||||
|  |                             fontSize: '12px', | ||||||
|  |                             fontWeight: 'bold', | ||||||
|  |                             color: '#fff', | ||||||
|  |                             lineHeight: '17px' | ||||||
|  |                           }" | ||||||
|  |                         ></avatarModule> | ||||||
|  |                         <div class="talk-read-list-item-info"> | ||||||
|  |                           <span style="font-size: 12px; font-weight: 600; line-height: 17px;">{{ | ||||||
|  |                             talkReadDetailItem.nickName | ||||||
|  |                           }}</span> | ||||||
|  |                           <span style="font-size: 12px; color: #999; line-height: 14px;">{{ | ||||||
|  |                             talkReadDetailItem.jobNum | ||||||
|  |                           }}</span> | ||||||
|  |                         </div> | ||||||
|  |                       </div> | ||||||
|  |                     </n-infinite-scroll> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </n-popover> | ||||||
|  |             </div> | ||||||
|           </main> |           </main> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
| @ -677,6 +1131,17 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       .talk_read_num { | ||||||
|  |         text-align: right; | ||||||
|  |         color: #7a58de; | ||||||
|  |         font-size: 12px; | ||||||
|  |         font-weight: 400; | ||||||
|  |         line-height: 17px; | ||||||
|  |         margin: 5px 0 0; | ||||||
|  |         span { | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       &:hover { |       &:hover { | ||||||
|         .talk-title { |         .talk-title { | ||||||
|           opacity: 1; |           opacity: 1; | ||||||
| @ -725,4 +1190,31 @@ const onContextMenuAvatar=(e:any,item:any)=>{ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .talk-read-list-detail { | ||||||
|  |   width: 341px; | ||||||
|  |   padding: 0 14px; | ||||||
|  | 
 | ||||||
|  |   .talk-read-list { | ||||||
|  |     .talk-read-list-item { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: row; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: flex-start; | ||||||
|  |       gap: 10px; | ||||||
|  |       padding: 10px 0; | ||||||
|  |       border-bottom: 1px solid #f1f1f1; | ||||||
|  | 
 | ||||||
|  |       .talk-read-list-item-info { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         align-items: flex-start; | ||||||
|  |         justify-content: center; | ||||||
|  | 
 | ||||||
|  |         span { | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
| @ -46,9 +46,9 @@ export default defineConfig(({ mode }) => { | |||||||
|       vueJsx({}),  |       vueJsx({}),  | ||||||
|       compressPlugin(),  |       compressPlugin(),  | ||||||
|       UnoCSS(), |       UnoCSS(), | ||||||
|       vueDevTools({ |       // vueDevTools({
 | ||||||
|         launchEditor: 'trae', |       //   launchEditor: 'trae',
 | ||||||
|       }) |       // })
 | ||||||
|     ], |     ], | ||||||
|     define: { |     define: { | ||||||
|       __APP_ENV__: env.APP_ENV |       __APP_ENV__: env.APP_ENV | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user