2024-12-24 08:14:21 +00:00
|
|
|
|
<script lang="ts" setup>
|
2025-05-27 10:49:48 +00:00
|
|
|
|
import { watch, onMounted, ref, nextTick, onUnmounted } from 'vue'
|
2025-05-28 07:40:36 +00:00
|
|
|
|
import { NDropdown, NCheckbox, NPopover, NInfiniteScroll } from 'naive-ui'
|
2024-12-24 08:14:21 +00:00
|
|
|
|
import { Loading, MoreThree, ToTop } from '@icon-park/vue-next'
|
|
|
|
|
import { bus } from '@/utils/event-bus'
|
|
|
|
|
import { useDialogueStore } from '@/store'
|
|
|
|
|
import { formatTime, parseTime } from '@/utils/datetime'
|
|
|
|
|
import { clipboard, htmlDecode, clipboardImage } from '@/utils/common'
|
|
|
|
|
import { downloadImage } from '@/utils/functions'
|
|
|
|
|
import { MessageComponents, ForwardableMessageType } from '@/constant/message'
|
|
|
|
|
import { useMenu } from './menu'
|
|
|
|
|
import SkipBottom from './SkipBottom.vue'
|
|
|
|
|
import { ITalkRecord } from '@/types/chat'
|
|
|
|
|
import { EditorConst } from '@/constant/event-bus'
|
|
|
|
|
import { useInject, useTalkRecord, useUtil } from '@/hooks'
|
2025-05-22 07:24:13 +00:00
|
|
|
|
import { ExclamationCircleFilled } from '@ant-design/icons-vue'
|
2025-05-27 10:49:48 +00:00
|
|
|
|
import { useUserStore, useUploadsStore } from '@/store'
|
2025-05-16 03:32:07 +00:00
|
|
|
|
import RevokeMessage from '@/components/talk/message/RevokeMessage.vue'
|
2025-05-28 07:40:36 +00:00
|
|
|
|
import { voiceToText, ServeMessageReadDetail } from '@/api/chat.js'
|
2025-05-27 10:49:48 +00:00
|
|
|
|
import { confirmBox } from '@/components/confirm-box/service.js'
|
|
|
|
|
import ws from '@/connect'
|
2025-05-28 07:40:36 +00:00
|
|
|
|
import avatarModule from '@/components/avatar-module/index.vue'
|
2025-05-27 10:49:48 +00:00
|
|
|
|
|
|
|
|
|
// 定义消息已读状态接口
|
|
|
|
|
interface ReadStatus {
|
|
|
|
|
msg_ids: string[]
|
|
|
|
|
talk_type: number
|
2025-05-28 07:40:36 +00:00
|
|
|
|
receiver_id: number
|
|
|
|
|
user_id?: number
|
2025-05-27 10:49:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定义状态接口
|
|
|
|
|
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
|
2025-05-28 07:40:36 +00:00
|
|
|
|
talkReadListDetail: any[]
|
|
|
|
|
readDetailIsUnread: number
|
|
|
|
|
currentMsgReadDetail: any | null
|
|
|
|
|
currentReadDetailPage: number
|
|
|
|
|
hasMoreReadListDetail: boolean
|
|
|
|
|
loadingReadListDetail: boolean
|
2025-05-27 10:49:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
const props = defineProps({
|
|
|
|
|
uid: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
talk_type: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
receiver_id: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
index_name: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: ''
|
2025-05-22 07:24:13 +00:00
|
|
|
|
},
|
|
|
|
|
specifiedMsg: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: ''
|
2025-05-27 10:49:48 +00:00
|
|
|
|
},
|
|
|
|
|
num: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
2024-12-24 08:14:21 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid)
|
2025-05-27 03:20:55 +00:00
|
|
|
|
const uploadsStore = useUploadsStore()
|
2024-12-24 08:14:21 +00:00
|
|
|
|
const { useMessage } = useUtil()
|
|
|
|
|
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
|
|
|
|
|
const { showUserInfoModal } = useInject()
|
|
|
|
|
const dialogueStore = useDialogueStore()
|
2025-05-15 08:07:56 +00:00
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
// const showUserInfoModal = (uid: number) => {
|
|
|
|
|
// userStore.getUserInfo(uid)
|
|
|
|
|
// }
|
2024-12-24 08:14:21 +00:00
|
|
|
|
// 置底按钮
|
|
|
|
|
const skipBottom = ref(false)
|
|
|
|
|
// 是否显示消息时间
|
|
|
|
|
const isShowTalkTime = (index: number, datetime: string) => {
|
|
|
|
|
if (datetime == undefined) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (records.value[index].is_revoke == 1) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
datetime = datetime.replace(/-/g, '/')
|
|
|
|
|
let time = Math.floor(Date.parse(datetime) / 1000)
|
|
|
|
|
let currTime = Math.floor(new Date().getTime() / 1000)
|
|
|
|
|
|
|
|
|
|
// 当前时间5分钟内时间不显示
|
|
|
|
|
if (currTime - time < 300) return false
|
|
|
|
|
|
|
|
|
|
// 判断是否是最后一条消息,最后一条消息默认显示时间
|
|
|
|
|
if (index == records.value.length - 1) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nextDate = records.value[index + 1].created_at.replace(/-/g, '/')
|
|
|
|
|
|
|
|
|
|
return !(
|
|
|
|
|
parseTime(new Date(datetime), '{y}-{m}-{d} {h}:{i}') ==
|
|
|
|
|
parseTime(new Date(nextDate), '{y}-{m}-{d} {h}:{i}')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 窗口滚动事件
|
|
|
|
|
const onPanelScroll = (e: any) => {
|
|
|
|
|
if (e.target.scrollTop == 0) {
|
|
|
|
|
onRefreshLoad()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const height = e.target.scrollTop + e.target.clientHeight
|
|
|
|
|
|
|
|
|
|
skipBottom.value = height < e.target.scrollHeight - 200
|
|
|
|
|
if (!skipBottom.value && dialogueStore.unreadBubble) {
|
|
|
|
|
dialogueStore.setUnreadBubble(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 10:49:48 +00:00
|
|
|
|
// 设置滚动状态
|
|
|
|
|
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()
|
2025-05-28 07:40:36 +00:00
|
|
|
|
}, 300) // 300ms 的防抖时间
|
2025-05-27 10:49:48 +00:00
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
// 检测是否到达底部
|
|
|
|
|
if (skipBottom.value == false) {
|
|
|
|
|
let len = dialogueStore.records.length
|
|
|
|
|
|
|
|
|
|
if (len > 100) {
|
|
|
|
|
// 为了优化数据过多页面卡顿,页面最多只显示100条数据
|
|
|
|
|
// 目前没有用虚拟列表只能这么干
|
|
|
|
|
dialogueStore.records.splice(0, len - 100)
|
|
|
|
|
|
|
|
|
|
let minSequence = 0
|
|
|
|
|
dialogueStore.records.forEach((item: ITalkRecord) => {
|
|
|
|
|
if (minSequence == 0 || item.sequence < minSequence) {
|
|
|
|
|
minSequence = item.sequence
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
loadConfig.cursor = minSequence
|
|
|
|
|
loadConfig.status = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 复制文本信息
|
|
|
|
|
const onCopyText = (data: ITalkRecord) => {
|
|
|
|
|
if (data.msg_type == 1) {
|
|
|
|
|
if (data.extra.content && data.extra.content.length > 0) {
|
|
|
|
|
return clipboard(htmlDecode(data.extra.content), () => useMessage.success('复制成功'))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.extra?.url) {
|
|
|
|
|
return clipboardImage(data.extra.url, () => {
|
|
|
|
|
useMessage.success('复制成功')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除对话消息
|
|
|
|
|
const onDeleteTalk = (data: ITalkRecord) => {
|
|
|
|
|
dialogueStore.ApiDeleteRecord([data.msg_id])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 撤销对话消息
|
|
|
|
|
const onRevokeTalk = (data: ITalkRecord) => {
|
|
|
|
|
dialogueStore.ApiRevokeRecord(data.msg_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 多选事件
|
|
|
|
|
const onMultiSelect = (data: ITalkRecord) => {
|
|
|
|
|
dialogueStore.updateDialogueRecord({
|
|
|
|
|
msg_id: data.msg_id,
|
|
|
|
|
isCheck: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
dialogueStore.isOpenMultiSelect = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDownloadFile = (data: ITalkRecord) => {
|
|
|
|
|
if (data.msg_type == 3) {
|
|
|
|
|
return downloadImage(data.extra.url, `${data.msg_id}.${data.extra.suffix}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.msg_type == 4) {
|
|
|
|
|
return useMessage.info('音频暂不支持下载!')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return useMessage.info('视频暂不支持下载!')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onQuoteMessage = (data: ITalkRecord) => {
|
|
|
|
|
let item = {
|
|
|
|
|
id: data.msg_id,
|
|
|
|
|
title: `${data.nickname} ${data.created_at}`,
|
|
|
|
|
describe: '',
|
|
|
|
|
image: ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (data.msg_type) {
|
|
|
|
|
case 1:
|
|
|
|
|
item.describe = data?.extra?.content
|
|
|
|
|
break // 文本消息
|
|
|
|
|
case 2:
|
|
|
|
|
item.describe = '[代码消息]'
|
|
|
|
|
break // 代码消息
|
|
|
|
|
case 3:
|
|
|
|
|
item.image = data.extra.url
|
|
|
|
|
break // 图片文件
|
|
|
|
|
case 4:
|
|
|
|
|
item.describe = '[语音文件]'
|
|
|
|
|
break // 语音文件
|
|
|
|
|
case 5:
|
|
|
|
|
item.describe = '[视频文件]'
|
|
|
|
|
break // 视频文件
|
|
|
|
|
case 6:
|
|
|
|
|
item.describe = '[其它文件]'
|
|
|
|
|
break // 其它文件
|
|
|
|
|
case 7:
|
|
|
|
|
item.describe = '[位置消息]'
|
|
|
|
|
break // 位置消息
|
|
|
|
|
case 8:
|
|
|
|
|
item.describe = '[名片消息]'
|
|
|
|
|
break // 名片消息
|
|
|
|
|
case 9:
|
|
|
|
|
item.describe = '[转发消息]'
|
|
|
|
|
break // 转发消息
|
|
|
|
|
case 10:
|
|
|
|
|
item.describe = '[登录消息]'
|
|
|
|
|
break // 登录消息
|
|
|
|
|
case 11:
|
|
|
|
|
item.describe = '[投票消息]'
|
|
|
|
|
break // 投票消息
|
|
|
|
|
case 12:
|
|
|
|
|
item.describe = '[图文消息]'
|
|
|
|
|
break // 图文消息
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bus.emit('editor:quote', item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onCollectImage = (data: ITalkRecord) => {
|
|
|
|
|
if (data.msg_type == 3) {
|
|
|
|
|
dialogueStore.ApiCollectImage({
|
|
|
|
|
msg_id: data.msg_id
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onClickNickname = (data: ITalkRecord) => {
|
|
|
|
|
bus.emit(EditorConst.Mention, {
|
|
|
|
|
id: data.user_id,
|
|
|
|
|
value: data.nickname
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 会话列表右键显示菜单
|
|
|
|
|
const onContextMenu = (e: any, item: ITalkRecord) => {
|
2025-05-27 10:49:48 +00:00
|
|
|
|
console.log('item', item)
|
2024-12-24 08:14:21 +00:00
|
|
|
|
if (!dialogueStore.isShowEditor || dialogueStore.isOpenMultiSelect) {
|
|
|
|
|
return e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showDropdownMenu(e, props.uid, item)
|
|
|
|
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
const onConvertText = async (data: ITalkRecord) => {
|
|
|
|
|
console.log('data', data)
|
2025-05-16 07:20:35 +00:00
|
|
|
|
data.is_convert_text = 1
|
2025-05-22 07:24:13 +00:00
|
|
|
|
const res = await voiceToText({ msgId: data.msg_id, voiceUrl: data.extra.url })
|
|
|
|
|
if (res.code == 200) {
|
2025-05-16 07:20:35 +00:00
|
|
|
|
data.extra.content = res.data.convText
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-22 07:24:13 +00:00
|
|
|
|
const onloseConvertText = (data: ITalkRecord) => {
|
2025-05-16 07:20:35 +00:00
|
|
|
|
data.is_convert_text = 0
|
|
|
|
|
}
|
2024-12-24 08:14:21 +00:00
|
|
|
|
const evnets = {
|
|
|
|
|
copy: onCopyText,
|
|
|
|
|
revoke: onRevokeTalk,
|
|
|
|
|
delete: onDeleteTalk,
|
|
|
|
|
multiSelect: onMultiSelect,
|
|
|
|
|
download: onDownloadFile,
|
|
|
|
|
quote: onQuoteMessage,
|
2025-05-16 07:20:35 +00:00
|
|
|
|
collect: onCollectImage,
|
|
|
|
|
convertText: onConvertText,
|
2025-05-22 07:24:13 +00:00
|
|
|
|
closeConvertText: onloseConvertText
|
2024-12-24 08:14:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 会话列表右键菜单回调事件
|
|
|
|
|
const onContextMenuHandle = (key: string) => {
|
|
|
|
|
// 触发事件
|
|
|
|
|
evnets[key] && evnets[key](dropdown.item)
|
|
|
|
|
closeDropdownMenu()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onRowClick = (item: ITalkRecord) => {
|
|
|
|
|
if (dialogueStore.isOpenMultiSelect) {
|
|
|
|
|
if (ForwardableMessageType.includes(item.msg_type)) {
|
|
|
|
|
item.isCheck = !item.isCheck
|
|
|
|
|
} else {
|
|
|
|
|
useMessage.info('此类消息不支持转发')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
const lastParams = ref('')
|
|
|
|
|
|
|
|
|
|
// 监听整个 props 对象的变化
|
|
|
|
|
watch(
|
|
|
|
|
() => props,
|
|
|
|
|
async (newProps) => {
|
|
|
|
|
await nextTick()
|
|
|
|
|
let specialParams = undefined
|
|
|
|
|
console.error(newProps, 'newProps')
|
|
|
|
|
if (newProps.specifiedMsg) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(decodeURIComponent(newProps.specifiedMsg))
|
|
|
|
|
// 只有会话id和参数都匹配才进入特殊模式
|
2025-05-27 10:49:48 +00:00
|
|
|
|
if (
|
|
|
|
|
parsed.talk_type === newProps.talk_type &&
|
|
|
|
|
parsed.receiver_id === newProps.receiver_id
|
|
|
|
|
) {
|
2025-05-22 07:24:13 +00:00
|
|
|
|
specialParams = parsed
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
onLoad(
|
|
|
|
|
{
|
|
|
|
|
receiver_id: newProps.receiver_id,
|
|
|
|
|
talk_type: newProps.talk_type,
|
|
|
|
|
limit: 30
|
|
|
|
|
},
|
|
|
|
|
specialParams ? { specifiedMsg: specialParams } : undefined
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true, deep: true }
|
|
|
|
|
)
|
2024-12-24 08:14:21 +00:00
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
// onMounted(() => {
|
2025-05-27 10:49:48 +00:00
|
|
|
|
// onLoad({ ...props, limit: 30 })
|
2025-05-22 07:24:13 +00:00
|
|
|
|
// })
|
2025-05-27 10:49:48 +00:00
|
|
|
|
const retry = (item: any) => {
|
2025-05-26 08:43:11 +00:00
|
|
|
|
confirmBox({
|
2025-05-27 10:49:48 +00:00
|
|
|
|
content: '确定重发吗'
|
|
|
|
|
}).then(() => {
|
|
|
|
|
uploadsStore.retryCommonUpload(item.extra.upload_id)
|
2025-05-27 03:20:55 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 10:49:48 +00:00
|
|
|
|
const onContextMenuAvatar = (e: any, item: any) => {
|
2025-05-27 03:20:55 +00:00
|
|
|
|
e.preventDefault()
|
2025-05-27 10:49:48 +00:00
|
|
|
|
if (item.float !== 'right') {
|
|
|
|
|
bus.emit(EditorConst.Mention, {
|
|
|
|
|
id: item.user_id,
|
|
|
|
|
value: item.nickname
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-05-26 08:43:11 +00:00
|
|
|
|
}
|
2025-05-27 10:49:48 +00:00
|
|
|
|
|
|
|
|
|
const state = ref<State>({
|
|
|
|
|
visibleElements: new Set(),
|
|
|
|
|
visibleOutElements: new Set(),
|
|
|
|
|
tempWaitDoRead: [],
|
|
|
|
|
tempWaitDoCheck: [],
|
|
|
|
|
setMessageReadInterval: null,
|
|
|
|
|
setOutMessageReadInterval: null,
|
|
|
|
|
lastUpdateTime: 0,
|
|
|
|
|
isScrolling: false,
|
|
|
|
|
scrollTimer: null,
|
2025-05-28 07:40:36 +00:00
|
|
|
|
lastTriggerTime: 0,
|
|
|
|
|
talkReadListDetail: [],
|
|
|
|
|
readDetailIsUnread: 1,
|
|
|
|
|
currentMsgReadDetail: null,
|
|
|
|
|
currentReadDetailPage: 1,
|
|
|
|
|
hasMoreReadListDetail: true,
|
|
|
|
|
loadingReadListDetail: false
|
2025-05-27 10:49:48 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 定义观察者变量
|
|
|
|
|
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))
|
|
|
|
|
}
|
2025-05-27 03:38:22 +00:00
|
|
|
|
}
|
2025-05-27 03:20:55 +00:00
|
|
|
|
|
2025-05-27 10:49:48 +00:00
|
|
|
|
// 检查需要获取别人发送的已读回执列表的元素
|
|
|
|
|
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监听已读回执,不轮询接口
|
2025-05-28 07:40:36 +00:00
|
|
|
|
watch(
|
|
|
|
|
() => state.value.visibleOutElements,
|
|
|
|
|
(newVal) => {
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
if (now - lastVisibleOutTriggerTime < 1000) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lastVisibleOutTriggerTime = now
|
|
|
|
|
checkVisibleOutElements()
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
deep: true,
|
|
|
|
|
immediate: true
|
2025-05-27 10:49:48 +00:00
|
|
|
|
}
|
2025-05-28 07:40:36 +00:00
|
|
|
|
)
|
2025-05-27 10:49:48 +00:00
|
|
|
|
|
|
|
|
|
// 观察者函数
|
|
|
|
|
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 }
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-28 07:40:36 +00:00
|
|
|
|
// 事件总线防抖处理
|
|
|
|
|
let eventBusDebounceTimer: number | null = null
|
|
|
|
|
|
2025-05-27 10:49:48 +00:00
|
|
|
|
onMounted(() => {
|
2025-05-28 07:40:36 +00:00
|
|
|
|
// 事件总线监听
|
|
|
|
|
bus.subscribe('check-visible-out-elements', (type) => {
|
|
|
|
|
if (eventBusDebounceTimer) {
|
|
|
|
|
clearTimeout(eventBusDebounceTimer)
|
|
|
|
|
}
|
|
|
|
|
eventBusDebounceTimer = window.setTimeout(() => {
|
|
|
|
|
checkVisibleOutElements()
|
|
|
|
|
eventBusDebounceTimer = null
|
|
|
|
|
}, 500)
|
|
|
|
|
})
|
2025-05-27 10:49:48 +00:00
|
|
|
|
//设置观察者前设置定时器
|
|
|
|
|
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()
|
|
|
|
|
}
|
2025-05-28 07:40:36 +00:00
|
|
|
|
// 清理事件总线防抖定时器
|
|
|
|
|
if (eventBusDebounceTimer) {
|
|
|
|
|
clearTimeout(eventBusDebounceTimer)
|
|
|
|
|
eventBusDebounceTimer = null
|
|
|
|
|
}
|
|
|
|
|
// 事件总线移除监听
|
|
|
|
|
bus.unsubscribe('check-visible-out-elements', checkVisibleOutElements)
|
2025-05-27 10:49:48 +00:00
|
|
|
|
})
|
2025-05-28 07:40:36 +00:00
|
|
|
|
|
|
|
|
|
//点击显示对应消息的已读回执详情
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<section class="section">
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
id="imChatPanel"
|
|
|
|
|
class="me-scrollbar me-scrollbar-thumb talk-container"
|
|
|
|
|
@scroll="onPanelScroll($event)"
|
|
|
|
|
>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<!-- 数据加载状态栏 -->
|
|
|
|
|
<div class="load-toolbar pointer">
|
|
|
|
|
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
|
|
|
|
|
<span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span>
|
|
|
|
|
<span v-else class="no-more"> 没有更多消息了 </span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
class="message-item"
|
|
|
|
|
v-for="(item, index) in records"
|
|
|
|
|
:key="item.msg_id"
|
|
|
|
|
:id="item.msg_id"
|
2025-05-27 10:49:48 +00:00
|
|
|
|
: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"
|
2025-05-22 07:24:13 +00:00
|
|
|
|
>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<!-- 系统消息 -->
|
|
|
|
|
<div v-if="item.msg_type >= 1000" class="message-box">
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<component
|
|
|
|
|
:is="MessageComponents[item.msg_type] || 'unknown-message'"
|
|
|
|
|
:extra="item.extra"
|
|
|
|
|
:data="item"
|
|
|
|
|
/>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 撤回消息 -->
|
|
|
|
|
<div v-else-if="item.is_revoke == 1" class="message-box">
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<revoke-message
|
|
|
|
|
:login_uid="uid"
|
|
|
|
|
:data="item"
|
|
|
|
|
:user_id="item.user_id"
|
|
|
|
|
:nickname="item.nickname"
|
|
|
|
|
:talk_type="item.talk_type"
|
|
|
|
|
:datetime="item.created_at"
|
|
|
|
|
/>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
v-else
|
|
|
|
|
class="message-box record-box"
|
|
|
|
|
:class="{
|
|
|
|
|
'direction-rt': item.float == 'right',
|
|
|
|
|
'multi-select': dialogueStore.isOpenMultiSelect,
|
|
|
|
|
'multi-select-check': item.isCheck
|
|
|
|
|
}"
|
|
|
|
|
>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<!-- 多选按钮 -->
|
2025-05-21 11:57:07 +00:00
|
|
|
|
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column shrink-0">
|
2025-05-27 10:49:48 +00:00
|
|
|
|
<n-checkbox
|
|
|
|
|
size="small"
|
|
|
|
|
:checked="item.isCheck"
|
|
|
|
|
@update:checked="item.isCheck = !item.isCheck"
|
|
|
|
|
/>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</aside>
|
|
|
|
|
<!-- 头像信息 -->
|
2025-05-22 07:24:13 +00:00
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<aside class="avatar-column">
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<im-avatar
|
|
|
|
|
class="pointer"
|
|
|
|
|
:src="item.avatar"
|
|
|
|
|
:size="42"
|
|
|
|
|
:username="item.nickname"
|
2025-05-27 03:20:55 +00:00
|
|
|
|
@contextmenu.prevent="onContextMenuAvatar($event, item)"
|
2025-05-22 07:24:13 +00:00
|
|
|
|
@click="showUserInfoModal(item.erp_user_id, item.user_id)"
|
|
|
|
|
/>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- 主体信息 -->
|
|
|
|
|
<main class="main-column">
|
2025-05-27 03:20:55 +00:00
|
|
|
|
<!-- <div class="talk-title">
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<span
|
|
|
|
|
class="nickname pointer"
|
|
|
|
|
v-show="talk_type == 2 && item.float == 'left'"
|
|
|
|
|
@click="onClickNickname(item)"
|
|
|
|
|
>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<span class="at">@</span>{{ item.nickname }}
|
|
|
|
|
</span>
|
2025-05-12 05:44:13 +00:00
|
|
|
|
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
|
2025-05-27 03:20:55 +00:00
|
|
|
|
</div> -->
|
|
|
|
|
<div class="talk-title">
|
2025-05-27 10:49:48 +00:00
|
|
|
|
<span class="mr-7px" v-show="talk_type == 2 && item.float == 'left'"
|
|
|
|
|
>{{ item.nickname }}
|
2025-05-27 03:20:55 +00:00
|
|
|
|
</span>
|
|
|
|
|
<span>{{ parseTime(item.created_at, '{y}/{m}/{d} {h}:{i}') }}</span>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</div>
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
class="talk-content"
|
|
|
|
|
:class="{ pointer: dialogueStore.isOpenMultiSelect }"
|
|
|
|
|
@click="onRowClick(item)"
|
|
|
|
|
>
|
|
|
|
|
<component
|
|
|
|
|
:is="MessageComponents[item.msg_type] || 'unknown-message'"
|
|
|
|
|
:extra="item.extra"
|
|
|
|
|
:data="item"
|
|
|
|
|
:max-width="true"
|
|
|
|
|
:source="'panel'"
|
|
|
|
|
@contextmenu.prevent="onContextMenu($event, item)"
|
|
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
v-if="
|
|
|
|
|
item.float === 'right' && item.extra.percentage === -1 && item.extra.is_uploading
|
|
|
|
|
"
|
|
|
|
|
class="mr-10px"
|
|
|
|
|
>
|
2025-05-26 08:58:12 +00:00
|
|
|
|
<n-button text style="font-size: 20px;" @click="retry(item)">
|
2025-05-15 08:07:56 +00:00
|
|
|
|
<n-icon color="#CF3050">
|
|
|
|
|
<ExclamationCircleFilled />
|
|
|
|
|
</n-icon>
|
2025-05-22 07:24:13 +00:00
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
2025-05-14 03:50:52 +00:00
|
|
|
|
<!-- <div class="talk-tools">
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<template v-if="talk_type == 1 && item.float == 'right'">
|
|
|
|
|
<loading
|
|
|
|
|
theme="outline"
|
|
|
|
|
size="19"
|
|
|
|
|
fill="#000"
|
|
|
|
|
:strokeWidth="1"
|
|
|
|
|
class="icon-rotate"
|
|
|
|
|
v-show="item.send_status == 1"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<span v-show="item.send_status == 1"> 正在发送... </span>
|
2025-05-14 03:50:52 +00:00
|
|
|
|
<span v-show="item.send_status != 1"> 已送达 </span>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
2025-05-15 08:07:56 +00:00
|
|
|
|
<n-icon class="more-tools pointer" :component="MoreThree" @click="onContextMenu($event, item)" />
|
|
|
|
|
</div> -->
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<div
|
|
|
|
|
v-if="item.extra.reply"
|
|
|
|
|
class="talk-reply pointer"
|
|
|
|
|
@click="onJumpMessage(item.extra?.reply?.msg_id)"
|
|
|
|
|
>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
<n-icon :component="ToTop" size="14" class="icon-top" />
|
|
|
|
|
<span class="ellipsis">
|
|
|
|
|
回复 {{ item.extra?.reply?.nickname }}:
|
|
|
|
|
{{ item.extra?.reply?.content }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-05-28 07:40:36 +00:00
|
|
|
|
|
|
|
|
|
<!-- 已读回执 -->
|
|
|
|
|
<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>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="datetime" v-show="isShowTalkTime(index, item.created_at)">
|
|
|
|
|
{{ formatTime(item.created_at) }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 置底按钮 -->
|
|
|
|
|
<SkipBottom v-model="skipBottom" />
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- 右键菜单 -->
|
2025-05-22 07:24:13 +00:00
|
|
|
|
<n-dropdown
|
|
|
|
|
:show="dropdown.show"
|
|
|
|
|
:x="dropdown.x"
|
|
|
|
|
:y="dropdown.y"
|
|
|
|
|
style="width: 142px;"
|
|
|
|
|
:options="dropdown.options"
|
|
|
|
|
@select="onContextMenuHandle"
|
|
|
|
|
@clickoutside="closeDropdownMenu"
|
|
|
|
|
/>
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
.section {
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.talk-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
padding: 10px 15px 30px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
|
|
|
|
.load-toolbar {
|
|
|
|
|
height: 38px;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
text-align: center;
|
|
|
|
|
line-height: 38px;
|
|
|
|
|
font-size: 13px;
|
2025-05-15 08:07:56 +00:00
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
.no-more {
|
|
|
|
|
color: #b9b3b3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-item {
|
|
|
|
|
&.border {
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
border: 1px solid var(--im-primary-color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-box {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 30px;
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.datetime {
|
|
|
|
|
height: 30px;
|
|
|
|
|
line-height: 30px;
|
|
|
|
|
color: #ccc9c9;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin: 5px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.record-box {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
|
|
|
|
.checkbox-column {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 35px;
|
|
|
|
|
order: 1;
|
|
|
|
|
user-select: none;
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-column {
|
2025-05-12 05:44:13 +00:00
|
|
|
|
width: 47px;
|
2024-12-24 08:14:21 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
order: 2;
|
|
|
|
|
user-select: none;
|
|
|
|
|
padding-top: 10px;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-column {
|
|
|
|
|
flex: 1 auto;
|
|
|
|
|
order: 3;
|
|
|
|
|
position: relative;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
padding: 5px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
min-height: 30px;
|
|
|
|
|
|
|
|
|
|
.talk-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
height: 24px;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
user-select: none;
|
|
|
|
|
color: #a7a0a0;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
|
|
|
|
&.show {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nickname {
|
|
|
|
|
color: var(--im-text-color);
|
|
|
|
|
margin-right: 5px;
|
2025-05-12 05:44:13 +00:00
|
|
|
|
font-size: 12px;
|
2025-05-15 08:07:56 +00:00
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
.at {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: var(--im-primary-color);
|
|
|
|
|
|
|
|
|
|
.at {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
transform: scale(0.88);
|
|
|
|
|
transform-origin: left center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.talk-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
.talk-tools {
|
|
|
|
|
display: flex;
|
|
|
|
|
margin: 0 8px;
|
|
|
|
|
color: #a79e9e;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
user-select: none;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
|
|
|
|
|
.more-tools {
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
margin-left: 5px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.talk-reply {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
align-items: center;
|
|
|
|
|
width: fit-content;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
margin-top: 3px;
|
|
|
|
|
margin-right: auto;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #8f8f8f;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
background-color: var(--im-message-left-bg-color);
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
.icon-top {
|
|
|
|
|
margin-right: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ellipsis {
|
|
|
|
|
display: -webkit-inline-box;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
-webkit-line-clamp: 3;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 07:40:36 +00:00
|
|
|
|
.talk_read_num {
|
|
|
|
|
text-align: right;
|
|
|
|
|
color: #7a58de;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
line-height: 17px;
|
|
|
|
|
margin: 5px 0 0;
|
|
|
|
|
span {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-24 08:14:21 +00:00
|
|
|
|
&:hover {
|
|
|
|
|
.talk-title {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.more-tools {
|
|
|
|
|
visibility: visible !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.direction-rt {
|
|
|
|
|
.avatar-column {
|
|
|
|
|
order: 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-column {
|
|
|
|
|
order: 2;
|
|
|
|
|
|
|
|
|
|
.talk-title {
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
transform-origin: right center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.talk-content {
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.talk-reply {
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.multi-select {
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
&.multi-select-check {
|
|
|
|
|
background-color: var(--im-active-bg-color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-28 07:40:36 +00:00
|
|
|
|
|
|
|
|
|
.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 {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-12-24 08:14:21 +00:00
|
|
|
|
</style>
|