完成新版已读回执规则接入,处理相应的socket消息监听、相关已读未读数量统计和详情列表展示

This commit is contained in:
wangyifeng 2025-05-28 15:40:36 +08:00
parent bdfd604fd9
commit 331ca65db6
7 changed files with 323 additions and 95 deletions

View File

@ -94,3 +94,8 @@ export const ServeConfirmVoteHandle = (data = {}) => {
export const ServeEmptyMessage = (data) => {
return post('/api/v1/talk/message/empty', data)
}
//获取消息已读未读详情
export const ServeMessageReadDetail = (data) => {
return post('/api/v1/talk/my-records/read/condition', data)
}

View File

@ -327,7 +327,7 @@ import { parseTime } from '@/utils/datetime'
import { fileFormatSize, fileSuffix } from '@/utils/strings'
import { NImage, NInfiniteScroll, NScrollbar, NIcon, NDatePicker } from 'naive-ui'
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray'])
const emits = defineEmits(['clearSearchMemberByAlphabet', 'getDisabledDateArray', 'hideSearchResultModal'])
const dialogueStore = useDialogueStore()
//
@ -736,15 +736,29 @@ const downloadAndOpenFile = (item) => {
//
const toDialogueByMember = async (msgInfo) => {
const sessionId = await getSessionId(dialogueParams.talk_type, dialogueParams.receiver_id)
uni.navigateTo({
url:
'/pages/dialog/index?sessionId=' +
sessionId +
'&keepDialogInfo=1' +
'&msgInfo=' +
encodeURIComponent(JSON.stringify(msgInfo))
console.error('====跳转到对应的记录位置====', msgInfo)
// , sequence
dialogueStore.specifiedMsg = encodeURIComponent(
JSON.stringify({
talk_type: msgInfo.talk_type,
receiver_id: msgInfo.receiver_id,
msg_id: msgInfo.msg_id,
cursor: msgInfo.sequence - 15 > 0 ? msgInfo.sequence - 15 : 0,
direction: 'down',
sort_sequence: 'asc',
create_time: msgInfo.created_at
})
)
emits('hideSearchResultModal')
// const sessionId = await getSessionId(dialogueParams.talk_type, dialogueParams.receiver_id)
// uni.navigateTo({
// url:
// '/pages/dialog/index?sessionId=' +
// sessionId +
// '&keepDialogInfo=1' +
// '&msgInfo=' +
// encodeURIComponent(JSON.stringify(msgInfo))
// })
}
//

View File

@ -7,6 +7,7 @@ import EventTalk from './event/talk'
import EventKeyboard from './event/keyboard'
import EventLogin from './event/login'
import EventRevoke from './event/revoke'
import EventRead from './event/read'
import { getAccessToken, isLoggedIn } from './utils/auth'
const urlCallback = () => {
@ -85,6 +86,7 @@ class Connect {
this.onImContactStatus()
this.onImMessageRevoke()
this.onImMessageKeyboard()
this.onImMessageListenRead()
this.onImMessageListenReadIncr()
}
@ -132,11 +134,14 @@ class Connect {
this.conn.on('im.message.revoke', (data: any) => new EventRevoke(data))
}
onImMessageListenRead() {
// 消息已读回执监听事件(全量)
this.conn.on('im.message.listen.read', (data: any) => new EventRead(data, 'total'))
}
onImMessageListenReadIncr() {
// 消息已读回执监听事件
this.conn.on('im.message.listen.read.incr', (data: any) => {
console.error('====接收到了新版已读回执增量=====', data)
})
// 消息已读回执监听事件(增量)
this.conn.on('im.message.listen.read.incr', (data: any) => new EventRead(data, 'incr'))
}
onImContactApply() {

54
src/event/read.js Normal file
View File

@ -0,0 +1,54 @@
import Base from './base'
import { useTalkStore, useDialogueStore } from '@/store'
import ws from '@/connect'
import { bus } from '@/utils/event-bus'
/**
* 已读回执事件
*/
class Read extends Base {
/**
* @var resource 资源
*/
resource
/**
* 场景类型
*/
type
/**
* 初始化构造方法
*
* @param {Object} resource Socket消息
*/
constructor(resource, type) {
super()
this.resource = resource
this.type = type
this.handle()
}
handle() {
if (this.type == 'total') {
console.error('====接收到了新版已读回执全量=====', this.resource)
const readList = this.resource.result
if (readList.length > 0) {
readList.forEach((item) => {
useDialogueStore().updateDialogueRecord({
msg_id: item.msg_id,
read_total_num: item.read_total_num
})
})
}
} else if (this.type == 'incr') {
console.error('====接收到了新版已读回执增量=====', this.resource)
// 由于直接使用增量的数值,会导致消息列表的已读回执数量不准确,可能多可能少
// 所以收到增量消息后,直接手动触发一次查询全量
bus.emit('check-visible-out-elements', 'incr')
}
}
}
export default Read

View File

@ -49,8 +49,8 @@ export interface ITalkRecord {
send_status: number
float: string,
is_convert_text?:number//语音记录的 是否是在展示转文本状态 1是 0否,
erp_user_id:number
erp_user_id:number,
read_total_num:number
}
export interface ITalkRecordExtraText {

View File

@ -481,6 +481,12 @@ const onDatePickShow = (show) => {
// state.nowDateTime = new Date()
}
}
//
const hideSearchResultModal = () => {
handleSearchRecordByConditionModalClose()
state.isShowGroupAside = false
}
</script>
<template>
@ -506,6 +512,7 @@ const onDatePickShow = (show) => {
:receiver_id="talkParams.receiver_id"
:index_name="talkParams.index_name"
:specifiedMsg="talkParams.specifiedMsg"
:num="talkParams.num"
/>
</main>
@ -706,6 +713,7 @@ const onDatePickShow = (show) => {
@getDisabledDateArray="getDisabledDateArray"
:selectedDateTime="state.selectedDateTime"
:nowDateTime="state.nowDateTime"
@hideSearchResultModal="hideSearchResultModal"
/>
</div>
</n-card>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
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 { bus } from '@/utils/event-bus'
import { useDialogueStore } from '@/store'
@ -16,16 +16,17 @@ import { useInject, useTalkRecord, useUtil } from '@/hooks'
import { ExclamationCircleFilled } from '@ant-design/icons-vue'
import { useUserStore, useUploadsStore } from '@/store'
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 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
receiver_id: number
user_id?: number
}
//
@ -40,6 +41,12 @@ interface State {
isScrolling: boolean
scrollTimer: number | null
lastTriggerTime: number
talkReadListDetail: any[]
readDetailIsUnread: number
currentMsgReadDetail: any | null
currentReadDetailPage: number
hasMoreReadListDetail: boolean
loadingReadListDetail: boolean
}
const props = defineProps({
@ -135,7 +142,7 @@ const onPanelScroll = (e: any) => {
checkVisibleOutElements()
//
lastVisibleOutTriggerTime = Date.now()
}, 150) // 150ms
}, 300) // 300ms
//
if (skipBottom.value == false) {
@ -387,47 +394,18 @@ const state = ref<State>({
lastUpdateTime: 0,
isScrolling: false,
scrollTimer: null,
lastTriggerTime: 0
lastTriggerTime: 0,
talkReadListDetail: [],
readDetailIsUnread: 1,
currentMsgReadDetail: null,
currentReadDetailPage: 1,
hasMoreReadListDetail: true,
loadingReadListDetail: false
})
// Map
const recordReadsMap = ref<Map<string, number>>(new Map())
//
let observer: IntersectionObserver | null = null
// Map UI
watch(
recordReadsMap,
(newMap) => {
requestAnimationFrame(() => {
newMap.forEach((readNum, msgId) => {
const element = document.getElementById(msgId)
if (element) {
element.dataset.readNum = String(readNum)
const readNumElement = element.querySelector('.have_read_num')
if (readNumElement) {
if (props.talk_type === 1) {
readNumElement.textContent = readNum > 0 ? '已读' : '未读'
} else {
readNumElement.textContent =
'已读 (' +
readNum +
'/' +
(Number(props.num) - 1 > 0 ? Number(props.num) - 1 : 0) +
')'
}
}
}
})
})
},
{
deep: true,
immediate: true
}
)
//
const checkVisibleElements = () => {
if (state.value.visibleElements.size > 0) {
@ -512,36 +490,8 @@ const checkVisibleOutElements = () => {
})
if (waitDoCheck.length > 0) {
waitDoCheck.forEach((doCheckItem) => {
// console.error('=========', doCheckItem)
console.error('====组装了新版已读回执参数需要发送socket=====', doCheckItem)
ws.emit('im.message.listen.read', doCheckItem)
// let params = Object.assign({}, doCheckItem, {
// talkType: doCheckItem.talk_type,
// receiverId: doCheckItem.talk_type === 1 ? props.uid : props.receiver_id,
// msgIds: doCheckItem.msg_ids,
// type: 'list'
// })
// const resp = ServeReadConditionList(params)
// resp
// .then(({ code, data }) => {
// if (code == 200) {
// if (Array.isArray(data.data)) {
// console.error('', data.data)
// data.data.forEach((item) => {
// if (item.msgId && item.readNum !== undefined) {
// recordReadsMap.value.set(item.msgId, item.readNum)
// }
// })
// } else if (data.data && data.data.readNum !== undefined) {
// console.error('', data.data)
// doCheckItem.msg_ids.forEach((msgId) => {
// recordReadsMap.value.set(msgId, data.data.readNum)
// })
// }
// }
// })
// .catch(() => {})
})
}
state.value.tempWaitDoCheck = JSON.parse(JSON.stringify(waitDoCheck))
@ -552,17 +502,21 @@ const checkVisibleOutElements = () => {
let lastVisibleOutTriggerTime = 0
//socket
watch(() => state.value.visibleOutElements, (newVal) => {
watch(
() => state.value.visibleOutElements,
(newVal) => {
const now = Date.now()
if (now - lastVisibleOutTriggerTime < 1000) {
return
}
lastVisibleOutTriggerTime = now
checkVisibleOutElements()
}, {
},
{
deep: true,
immediate: true
})
}
)
//
const handleIntersection = (entries) => {
@ -617,7 +571,20 @@ watch(
{ 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)
@ -669,7 +636,77 @@ onUnmounted(() => {
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', //listdetail
talkType: state?.value?.currentMsgReadDetail?.talk_type, //12
receiverId: state?.value?.currentMsgReadDetail?.receiver_id, //idid
msgId: state?.value?.currentMsgReadDetail?.msg_id,
isUnread: state?.value?.readDetailIsUnread //01
}
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>
<template>
@ -821,6 +858,72 @@ onUnmounted(() => {
{{ item.extra?.reply?.content }}
</span>
</div>
<!-- 已读回执 -->
<div class="talk_read_num" v-if="item.user_id === props.uid">
<n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;">
<template #trigger>
<span v-if="props.talk_type === 1">{{
item.read_total_num > 0 ? '已读' : '未读'
}}</span>
<span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)">
已读 ({{ 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>
</div>
@ -1019,6 +1122,18 @@ onUnmounted(() => {
}
}
.talk_read_num {
text-align: right;
color: #7a58de;
font-size: 12px;
font-weight: 400;
line-height: 17px;
margin: 5px 0 0;
span {
cursor: pointer;
}
}
&:hover {
.talk-title {
opacity: 1;
@ -1067,4 +1182,31 @@ onUnmounted(() => {
}
}
}
.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>