@ -1,6 +1,6 @@
< script lang = "ts" setup >
import { watch , onMounted , ref , nextTick } from 'vue'
import { NDropdown , NCheckbox } from 'naive-ui'
import { watch , onMounted , ref , nextTick , onUnmounted } from 'vue'
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'
@ -14,10 +14,42 @@ import { ITalkRecord } from '@/types/chat'
import { EditorConst } from '@/constant/event-bus'
import { useInject , useTalkRecord , useUtil } from '@/hooks'
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 { voiceToText } from '@/api/chat.js'
import { confirmBox } from '@/components/confirm-box/service.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
}
/ / 定 义 状 态 接 口
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 ( {
uid : {
type : Number ,
@ -38,10 +70,14 @@ const props = defineProps({
specifiedMsg : {
type : String ,
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 { useMessage } = useUtil ( )
const { dropdown , showDropdownMenu , closeDropdownMenu } = useMenu ( )
@ -90,11 +126,33 @@ const onPanelScroll = (e: any) => {
}
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 ) {
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 ) / / 3 0 0 m s 的 防 抖 时 间
/ / 检 测 是 否 到 达 底 部
if ( skipBottom . value == false ) {
let len = dialogueStore . records . length
@ -231,7 +289,6 @@ const onClickNickname = (data: ITalkRecord) => {
/ / 会 话 列 表 右 键 显 示 菜 单
const onContextMenu = ( e : any , item : ITalkRecord ) => {
if ( ! dialogueStore . isShowEditor || dialogueStore . isOpenMultiSelect ) {
return e . preventDefault ( )
}
@ -294,7 +351,10 @@ watch(
try {
const parsed = JSON . parse ( decodeURIComponent ( newProps . specifiedMsg ) )
/ / 只 有 会 话 i d 和 参 数 都 匹 配 才 进 入 特 殊 模 式
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
}
} catch ( e ) { }
@ -312,29 +372,350 @@ watch(
)
/ / o n M o u n t e d ( ( ) = > {
/ / o n L o a d ( { . . . p r o p s , l i m i t : 3 0 } )
/ / o n L o a d ( { . . . p r o p s , l i m i t : 3 0 } )
/ / } )
const retry = ( item : any ) => {
const retry = ( item : any ) => {
confirmBox ( {
content : '确定重发吗'
} ) . then ( ( ) => {
uploadsStore . retryCommonUpload ( item . extra . upload _id )
content : '确定重发吗'
} ) . then ( ( ) => {
uploadsStore . retryCommonUpload ( item . extra . upload _id )
} )
}
const onContextMenuAvatar = ( e : any , item : any ) => {
if ( item . talk _type !== 1 ) {
e . preventDefault ( )
if ( item . float !== 'right' ) {
bus . emit ( EditorConst . Mention , {
id : item . user _id ,
value : item . nickname
} )
}
const onContextMenuAvatar = ( e : any , item : any ) => {
e . preventDefault ( )
if ( item . float !== 'right' ) {
bus . emit ( EditorConst . Mention , {
id : item . user _id ,
value : item . nickname
} )
}
}
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
/ / 新 版 采 用 s o c k e t 监 听 已 读 回 执 , 不 轮 询 接 口
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
}
/ / 旧 版 采 用 定 时 器 来 轮 询 已 读 回 执 , 新 版 采 用 s o c k e t 监 听 已 读 回 执
/ / s t a t e . v a l u e . s e t O u t M e s s a g e R e a d I n t e r v a l = s e t I n t e r v a l ( ( ) = > {
/ / c h e c k V i s i b l e O u t E l e m e n t s ( )
/ / } , 2 0 0 0 )
/ / 初 始 化 设 置 观 察 者
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' , / / l i s t 是 列 表 , d e t a i l 是 详 情
talkType : state ? . value ? . currentMsgReadDetail ? . talk _type , / / 1 私 聊 2 群 聊
receiverId : state ? . value ? . currentMsgReadDetail ? . receiver _id , / / 私 聊 的 时 候 是 对 方 用 户 i d , 群 聊 的 时 候 是 对 方 群 i d
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
}
}
}
/ / 已 读 回 执 t a b 切 换
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 >
@ -356,6 +737,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
v - for = "(item, index) in records"
: key = "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" >
@ -389,7 +775,11 @@ const onContextMenuAvatar=(e:any,item:any)=>{
>
<!-- 多选按钮 -- >
< 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 >
<!-- 头像信息 -- >
@ -417,10 +807,8 @@ const onContextMenuAvatar=(e:any,item:any)=>{
< span > { { parseTime ( item . created _at , '{y}/{m}/{d} {h}:{i}' ) } } < / span >
< / div > -- >
< div class = "talk-title" >
< span class = "mr-7px"
v - show = "talk_type == 2 && item.float == 'left'"
> { { item . nickname } }
< span class = "mr-7px" v -show = " talk_type = = 2 & & item.float = = ' left ' "
> { { item . nickname } }
< / span >
< span > { { parseTime ( item . created _at , '{y}/{m}/{d} {h}:{i}' ) } } < / span >
< / div >
@ -479,6 +867,72 @@ const onContextMenuAvatar=(e:any,item:any)=>{
{ { item . extra ? . reply ? . content } }
< / span >
< / 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 - t a b >
< n -tab name = "read-tab" >
{ { ` 已读( ${ item . read _total _num || 0 } ) ` } }
< / n - t a b >
< / n - t a b s >
< 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 - i n f i n i t e - s c r o l l >
< / div >
< / div >
< / n - p o p o v e r >
< / div >
< / main >
< / div >
@ -677,6 +1131,17 @@ const onContextMenuAvatar=(e:any,item:any)=>{
}
}
. talk _read _num {
text - align : right ;
color : # 7 a58de ;
font - size : 12 px ;
font - weight : 400 ;
line - height : 17 px ;
margin : 5 px 0 0 ;
span {
}
}
& : hover {
. talk - title {
opacity : 1 ;
@ -725,4 +1190,31 @@ const onContextMenuAvatar=(e:any,item:any)=>{
}
}
}
. talk - read - list - detail {
width : 341 px ;
padding : 0 14 px ;
. talk - read - list {
. talk - read - list - item {
display : flex ;
flex - direction : row ;
align - items : center ;
justify - content : flex - start ;
gap : 10 px ;
padding : 10 px 0 ;
border - bottom : 1 px solid # f1f1f1 ;
. talk - read - list - item - info {
display : flex ;
flex - direction : column ;
align - items : flex - start ;
justify - content : center ;
span {
}
}
}
}
}
< / style >