Files
open-anylink-web/src/views/message/MessageLayout.vue
2025-03-29 21:59:35 +08:00

1480 lines
46 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
eslint-disable prettier/prettier
<script setup>
import { ref, onMounted, onUnmounted, computed, nextTick, watch } from 'vue'
import {
Phone,
VideoCamera,
MoreFilled,
CirclePlus,
ArrowDownBold,
ArrowUp
} from '@element-plus/icons-vue'
import DragLine from '@/components/common/DragLine.vue'
import SearchBox from '@/components/search/SearchBox.vue'
import AddButton from '@/components/common/AddButton.vue'
import SessionItem from '@/views/message/components/SessionItem.vue'
import InputToolBar from '@/views/message/components/InputToolBar.vue'
import InputEditor from '@/views/message/components/InputEditor.vue'
import MessageItem from '@/views/message/components/MessageItem.vue'
import SessionTag from '@/views/message/components/SessionTag.vue'
import SelectUserDialog from '@/components/common/SelectUserDialog.vue'
import {
useUserStore,
useSettingStore,
useMessageStore,
useUserCardStore,
useGroupCardStore,
useGroupStore
} from '@/stores'
import backgroupImage from '@/assets/svg/messagebx_bg.svg'
import {
msgChatPullMsgService,
msgChatCreateSessionService,
msgChatQuerySessionService
} from '@/api/message'
import { groupInfoService, groupCreateService } from '@/api/group'
import { MsgType } from '@/proto/msg'
import wsConnect from '@/js/websocket/wsConnect'
import { onReceiveChatMsg, onReceiveGroupChatMsg, onReceiveGroupSystemMsg } from '@/js/event'
import { userQueryService } from '@/api/user'
import { ElLoading, ElMessage } from 'element-plus'
import { el_loading_options } from '@/const/commonConst'
import { combineId, sessionIdConvert } from '@/js/utils/common'
import SessionMenu from '@/views/message/components/SessionMenu.vue'
import router from '@/router'
import { BEGIN_MSG_ID, msgContentType, msgSendStatus } from '@/const/msgConst'
import EditDialog from '@/components/common/EditDialog.vue'
import AddOprMenu from '@/views/message/components/AddOprMenu.vue'
import MessageGroupRightSide from '@/views/message/components/MessageGroupRightSide.vue'
import HashNoData from '@/components/common/HasNoData.vue'
import InputRecorder from '@/views/message/components/InputRecorder.vue'
import { playMsgSend } from '@/js/utils/audio'
const userData = useUserStore()
const settingData = useSettingStore()
const messageData = useMessageStore()
const userCardData = useUserCardStore()
const groupCardData = useGroupCardStore()
const groupData = useGroupStore()
const sessionListRef = ref()
const asideWidth = ref(0)
const asideWidthMin = 200
const asideWidthMax = 500
const inputBoxHeight = ref(0)
const inputBoxHeightMin = 200
const inputBoxHeightMax = 500
const msgListDiv = ref()
const disToBottom = ref(0)
const nearBottomDis = 50
const newMsgTips = ref({
isShowTopTips: false,
isShowBottomTips: false,
unreadCount: 0,
firstElement: null
})
const inputToolBarRef = ref()
const myAccount = computed(() => {
return userData.user.account
})
//当前被选中的session
const selectedSessionId = computed(() => {
return messageData.selectedSessionId || ''
})
// 消息拉取是否结束
const pullMsgDone = computed(() => {
return selectedSession.value.pullMsgDone || false
})
const msgIdSortArray = computed(() => {
return messageData.msgIdSortArray[selectedSessionId.value]
})
// 缓存的消息列表是否为空注意和hasNoMoreMsg的区别
const noMsg = computed(() => {
return msgIdSortArray.value?.length === 0
})
// 当前session的第一条消息ID
const firstMsgId = computed(() => {
if (!noMsg.value) {
return msgIdSortArray.value[0]
} else {
return 0
}
})
// 当前session的最后一条消息ID
const lastMsgId = computed(() => {
if (!noMsg.value) {
const len = msgIdSortArray.value?.length
return len ? msgIdSortArray.value[len - 1] : 0
} else {
return 0
}
})
// 是否是没有更多消息了从服务器拉取结束了或者firstMsgId是BEGIN_MSG_ID
const hasNoMoreMsg = computed(() => {
return pullMsgDone.value || firstMsgId.value === BEGIN_MSG_ID
})
const groupMembers = computed(() => {
return groupData.groupMembersList[selectedSession.value?.remoteId]
})
const isNotInGroup = computed(() => {
return selectedSession.value.sessionType === MsgType.GROUP_CHAT && selectedSession.value.leave
})
const isMutedInGroup = computed(() => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
const groupInfo = groupData.groupInfoList[selectedSession.value.remoteId]
const me = groupMembers.value[myAccount.value]
if (me.mutedMode === 1 || (groupInfo.allMuted && me.mutedMode !== 2)) {
return true
} else {
return false
}
} else {
return false
}
})
const isShowReturnBottom = ref(false)
// 留在该页面上的session状态缓存例如
// isLoading: 正在加载数据解释会话首次被打开时开场加载数据的loading场景
// isLoadMoreLoading: 是否加载更多中解释会话被打开后向上移动滚轮到顶出现“加载更多”字样继续滚动或者点击的loading场景
// 这些数据不能放在messageData因为这个cache会随页面消亡而清除数据重新回到页面后使用初始默认数据即可
// 数据格式示例:{'sessionId_xxx': {isLoadMoreLoading: false, isLoading: false}}
// 触发选中session事件后才会给这个数据里面插入被选中session状态的缓存
const selectedSessionCache = ref({})
const capacity = ref(15) //TODO 现在是调试值
const step = 15 //TODO 现在是调试值
const startIndex = computed(() => {
if (selectedSessionId.value) {
const len = msgIdSortArray.value?.length
return len > capacity.value ? len - capacity.value : 0
} else {
return 0
}
})
const initSession = (sessionId) => {
capacity.value = 15 //会话的默认显示消息记录数
msgListReachBottom() //会话默认滚到最底部
isShowReturnBottom.value = false //会话默认不弹出“返回底部”的按钮
// 如果selectedSessionCache有这个sessionId就不重置
if (!selectedSessionCache.value[sessionId]) {
selectedSessionCache.value[sessionId] = {
isLoading: false,
isLoadMoreLoading: false
}
}
isShowRecorder.value = false // 麦克风输入状态重置
inputRecorderRef.value?.cancelSend() // 取消音频发送
}
/**
* 定位的session的位置
* 这里受限sessionListSorted的排序速度如果定位的时候排序没有完成定位的位置就不对
* @param sessionId
*/
const locateSession = (sessionId) => {
let task
let count = 0
task = setInterval(() => {
if (count >= 3) clearInterval(task)
const selectedElement = document.querySelector(`#session-item-${sessionIdConvert(sessionId)}`)
// 如果被选中元素的上边在scrollTop之的上面或这在下边在scrollTop+clientHeight的下面显示不全或者完全没有显示则需要重新定位
// 由于offsetTop和offsetHeight不包含外边距因此定位存在细小误差暂不处理
if (selectedElement.offsetTop - selectedElement.offsetHeight < sessionListRef.value.scrollTop) {
sessionListRef.value.scrollTop = selectedElement.offsetTop - selectedElement.offsetHeight
} else if (
selectedElement.offsetTop >
sessionListRef.value.scrollTop + sessionListRef.value.clientHeight
) {
sessionListRef.value.scrollTop = selectedElement.offsetTop - sessionListRef.value.clientHeight
}
count++
}, 200)
}
const msgIdsShow = computed(() => {
const ids = msgIdSortArray.value?.slice(startIndex.value)
if (!ids) return []
return ids
})
let lastReadMsgId = 0
const msgExtend = computed(() => {
const data = {}
for (let index = 0; index < msgIdsShow.value.length; index++) {
const ext = {}
if (index > 0) {
const preMsg = messageData.getMsg(selectedSessionId.value, msgIdsShow.value[index - 1])
// 上一条消息的时间相邻的时间只出一条tips
ext['preMsgTime'] = preMsg.msgTime
// 判断是否是打开session后的第一条未读消息
if (preMsg.msgId === lastReadMsgId) {
ext['isFirstNew'] = true
} else {
ext['isFirstNew'] = false
}
} else {
ext['preMsgTime'] = null
ext['isFirstNew'] = false
}
data[msgIdsShow.value[index]] = ext
}
return data
})
const selectedSession = computed(() => {
return messageData.sessionList[selectedSessionId.value] || {}
})
onMounted(async () => {
await messageData.loadSessionList()
await groupData.loadGroupInfoList()
messageData.loadPartitions() // 异步加载
asideWidth.value = settingData.sessionListDrag[myAccount.value] || 300
inputBoxHeight.value = settingData.inputBoxDrag[myAccount.value] || 300
wsConnect.bindEvent(MsgType.CHAT, onReceiveChatMsg(updateScroll, capacity)) //绑定接收Chat消息的事件
wsConnect.bindEvent(MsgType.GROUP_CHAT, onReceiveGroupChatMsg(updateScroll, capacity)) //绑定接收GroupChat消息的事件
wsConnect.bindGroupSystemMsgEvent(onReceiveGroupSystemMsg(updateScroll, capacity)) //绑定接收群系统消息事件
// 这里要接收从其他页面跳转过来传递的sessionId参数
const routerSessionId = router.currentRoute.value.query.sessionId
if (routerSessionId) {
if (routerSessionId in messageData.sessionList) {
handleSelectedSession(routerSessionId)
} else {
msgChatQuerySessionService({ sessionId: routerSessionId })
.then((res) => {
if (res.data.data) {
messageData.addSession(res.data.data.session)
handleSelectedSession(routerSessionId)
}
})
.catch(() => {
router.replace({ query: {} })
})
}
}
})
onUnmounted(() => {
messageData.setSelectedSessionId('')
})
const handleMsgListWheel = async () => {
if (
msgListDiv.value.scrollTop === 0 &&
!selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading
) {
await onLoadMore()
}
const clientHeight = document.querySelector('.message-main').clientHeight
disToBottom.value = msgListDiv.value.scrollHeight - msgListDiv.value.scrollTop - clientHeight
// disToBottom接近50个像素的时候关闭底部未读tips控件
newMsgTips.value.isShowBottomTips =
disToBottom.value < nearBottomDis ? false : newMsgTips.value.isShowBottomTips
// isShowReturnBottom.value = disToBottom.value > 300 // 控制是否显示"回到底部"的按钮。暂时取消这个提示功能,与消息提示的按钮显得有点重复
if (newMsgTips.value.firstElement?.getBoundingClientRect().top > 0) {
newMsgTips.value.isShowTopTips = false
}
}
// 把sessionList转成数组并按照msgTime排序
const sessionListSorted = computed(() => {
if (!Object.keys(messageData.sessionList)) {
return []
} else {
let sessionArr = Object.values(messageData.sessionList)
return sessionArr.sort((a, b) => {
if (a.top && !b.top) {
// 排序第一优先级:是否置顶
return -1
} else if (!a.top && b.top) {
return 1
} else {
if (a.draft && !b.draft) {
// 排序第二优先级:是否有草稿
return -1
} else if (!a.draft && b.draft) {
return 1
} else {
// 排序第三优先级:最后一条消息的时间
const a_msgIds = messageData.msgIdSortArray[a.sessionId]
const a_msgIds_len = a_msgIds?.length
if (!a_msgIds_len) return 1
const a_lastMsg = messageData.getMsg(a.sessionId, a_msgIds[a_msgIds_len - 1])
const b_msgIds = messageData.msgIdSortArray[b.sessionId]
const b_msgIds_len = b_msgIds?.length
if (!b_msgIds_len) return -1
const b_lastMsg = messageData.getMsg(b.sessionId, b_msgIds[b_msgIds_len - 1])
const bTime = new Date(b_lastMsg.msgTime).getTime()
const aTIme = new Date(a_lastMsg.msgTime).getTime()
if (bTime !== aTIme) {
return bTime - aTIme
}
}
}
})
}
})
const showName = computed(() => {
switch (selectedSession.value.sessionType) {
case MsgType.CHAT:
return selectedSession.value.objectInfo.nickName
case MsgType.GROUP_CHAT:
return (
groupData.groupInfoList[selectedSession.value.remoteId]?.groupName ||
selectedSession.value.objectInfo.groupName
)
default:
return ''
}
})
const showId = computed(() => {
return selectedSession.value.remoteId
})
const getMsgSenderObj = (msgId) => {
const msg = messageData.getMsg(selectedSessionId.value, msgId)
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
// 如果此时memberList还没有加载完成先return account给MessageItem子组件
return groupMembers.value ? groupMembers.value[msg.fromId] : { account: msg.fromId }
} else {
if (myAccount.value === msg.fromId) {
return userData.user
} else {
return selectedSession.value.objectInfo
}
}
}
const onAsideDragUpdate = ({ width }) => {
asideWidth.value = width
settingData.setSessionListDrag({
...settingData.sessionListDrag,
[myAccount.value]: width
})
}
const onInputBoxDragUpdate = ({ height }) => {
inputBoxHeight.value = height
msgListReachBottom('smooth')
settingData.setInputBoxDrag({
...settingData.inputBoxDrag,
[myAccount.value]: height
})
}
/**
* 从服务端拉取消息endMsgId有值就表示最大拉到endMsgId-1的消息(滚轮上滚加载更多消息场景)
* @param ref 标记更新的msgId位置
*/
const pullMsg = async (endMsgId = null) => {
// 下列三种情况不拉取数据
if (
hasNoMoreMsg.value ||
selectedSessionCache.value[selectedSessionId.value].isLoading ||
selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading
) {
return
}
const pageSize = 30
const params = {
sessionId: selectedSessionId.value,
pageSize: pageSize,
endMsgId: endMsgId
}
if (!endMsgId) selectedSessionCache.value[selectedSessionId.value].isLoading = true
// 显示"加载更多中..."
selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading = true
// 这里一定不要响应式的sessionId否则快速点击切换session会导致数据都叠加到最后一次的selectedSessionId上面
const sessionId = selectedSessionId.value
try {
const res = await msgChatPullMsgService(params)
const msgCount = res.data.data.count
if (msgCount > 0) {
await messageData.addMsgRecords(sessionId, res.data.data.msgList)
}
if (msgCount < pageSize) {
messageData.updateSession({
sessionId: sessionId,
pullMsgDone: true
})
}
} finally {
selectedSessionCache.value[selectedSessionId.value].isLoading = false
selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading = false
}
}
// 表示有个session被选中了
const handleSelectedSession = async (sessionId) => {
router.replace({ query: { sessionId: sessionId } })
if (selectedSessionId.value !== sessionId) {
messageData.setSelectedSessionId(sessionId)
initSession(sessionId)
locateSession(sessionId)
// 如果是群组要加载成员列表显示消息需要accountnickNameavatar信息
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
// 没有members数据才需要加载成员列表加载过了就不重复加载了
if (!groupMembers.value) {
const res = await groupInfoService({ groupId: selectedSession.value.remoteId })
groupData.setGroupInfo({
groupId: selectedSession.value.remoteId,
groupInfo: res.data.data.groupInfo || {}
})
groupData.setGroupMembers({
groupId: selectedSession.value.remoteId,
members: res.data.data.members || {}
})
}
}
lastReadMsgId = selectedSession.value.readMsgId //保存这个readMsgId,要留给MessageItem用
handleRead()
}
}
const handleRead = () => {
if (selectedSessionId.value && selectedSession.value.readMsgId < lastMsgId.value) {
const content = lastMsgId.value.toString()
const msgType =
selectedSession.value.sessionType === MsgType.CHAT
? MsgType.CHAT_READ
: MsgType.GROUP_CHAT_READ
wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', '', () => {})
// 更新本地缓存的已读位置
messageData.updateSession({
sessionId: selectedSessionId.value,
readMsgId: content,
readTime: new Date(),
unreadCount: 0
})
}
}
const handleSendMessage = (content, resendSeq = '') => {
if (isNotInGroup.value) {
ElMessage.warning('您已离开该群或群已被解散')
return
}
if (isMutedInGroup.value) {
ElMessage.warning('您已被禁言,请联系管理员')
return
}
if (inputToolBarRef.value) inputToolBarRef.value.closeWindow()
const msg = {
sessionId: selectedSessionId.value,
fromId: myAccount.value,
msgType: selectedSession.value.sessionType,
content: content,
status: msgSendStatus.PENDING,
msgTime: new Date(),
sendTime: new Date()
}
const resendInterval = 2000 //2秒
const before = async (seq, data) => {
// 当2s内status如果还是pending中则重发3次。如果最后还是pending则把status置为failed
setTimeout(() => {
if (msg.status === msgSendStatus.PENDING) {
wsConnect.sendAgent(data)
setTimeout(() => {
if (msg.status === msgSendStatus.PENDING) {
wsConnect.sendAgent(data)
setTimeout(() => {
if (msg.status === msgSendStatus.PENDING) {
wsConnect.sendAgent(data)
setTimeout(async () => {
if (msg.status === msgSendStatus.PENDING) {
messageData.removeMsgRecord(msg.sessionId, msg.msgId)
msg.status = msgSendStatus.FAILED
await messageData.addMsgRecords(msg.sessionId, [msg])
ElMessage.error('消息发送失败')
}
}, resendInterval)
}
}, resendInterval)
}
}, resendInterval)
}
}, resendInterval)
messageData.updateSession({
sessionId: msg.sessionId,
unreadCount: 0, // 最后一条消息是自己发的因此未读是0
draft: '' //草稿意味着要清空
})
msg.seq = seq
msg.msgId = seq //服务器没有回复DELIVERED消息之前都用seq暂代msgId
await messageData.addMsgRecords(msg.sessionId, [msg])
}
const after = async (msgId) => {
messageData.updateSession({
sessionId: msg.sessionId,
readMsgId: msgId, // 最后一条消息是自己发的因此已读更新到刚发的这条消息的msgId
readTime: new Date()
})
messageData.removeMsgRecord(msg.sessionId, msg.msgId) //移除seq为key的msg
msg.msgId = msgId
msg.status = msgSendStatus.OK
await messageData.addMsgRecords(msg.sessionId, [msg]) //添加服务端返回msgId为key的msg
if (!messageData.sessionList[msg.sessionId].dnd) {
playMsgSend()
}
}
wsConnect.sendMsg(
msg.sessionId,
showId.value,
selectedSession.value.sessionType,
content,
resendSeq,
before,
after
)
capacity.value++
msgListReachBottom()
locateSession(msg.sessionId)
}
const handleResendMessage = ({ content, seq }) => {
handleSendMessage(content, seq)
}
const onLoadMore = async () => {
const scrollHeight = msgListDiv.value.scrollHeight
const scrollTop = msgListDiv.value.scrollTop
if (msgIdSortArray.value?.length <= capacity.value) {
await pullMsg(msgIdsShow.value[0])
}
const len = msgIdSortArray.value?.length
if (len > capacity.value) {
if (len - capacity.value > step) {
capacity.value += step
} else {
capacity.value = len
}
}
// 保持页面对话的锚定位置
nextTick(() => {
msgListDiv.value.scrollTop = msgListDiv.value.scrollHeight - scrollHeight + scrollTop
})
}
const updateScroll = () => {
if (disToBottom.value < nearBottomDis) {
msgListReachBottom('smooth')
}
}
/**
* 消息列表拉到最底部
* @param behavior smooth 平滑的, instant 立即(默认)
*/
const msgListReachBottom = (behavior = 'instant') => {
setTimeout(() => {
msgListDiv.value?.scrollTo({
top: msgListDiv.value.scrollHeight,
behavior: behavior
})
newMsgTips.value.isShowBottomTips = false
disToBottom.value = 0
}, 0)
}
const onReturnBottom = () => {
msgListReachBottom('smooth')
}
const onReachFirstUnReadMsg = () => {
const msgListRect = msgListDiv.value.getBoundingClientRect()
const firstElRect = newMsgTips.value.firstElement.getBoundingClientRect()
nextTick(() => {
msgListDiv.value.scrollTop = msgListDiv.value.scrollTop - (msgListRect.top - firstElRect.top)
})
newMsgTips.value.isShowTopTips = false
}
const onClickMsgContainer = () => {
handleRead()
}
const onShowUserCard = ({ sessionId, account }) => {
const loadingInstance = ElLoading.service(el_loading_options)
if (myAccount.value === account) {
userData
.updateUser()
.then(() => {
userCardData.setUserInfo(userData.user)
userCardData.setIsShow(true)
})
.finally(() => {
//防止请求异常导致loading关不掉
loadingInstance.close()
})
} else {
userQueryService({ account: account })
.then((res) => {
userCardData.setUserInfo(res.data.data)
userCardData.setIsShow(true)
const sessionIdWithThisAccount = combineId(account, myAccount.value)
// 如果有和这个用户的session则更新一下session
if (sessionIdWithThisAccount in messageData.sessionList) {
messageData.updateSession({
sessionId: sessionIdWithThisAccount,
objectInfo: {
...messageData.sessionList[sessionIdWithThisAccount].objectInfo,
nickName: res.data.data.nickName,
signature: res.data.data.signature,
avatar: res.data.data.avatar,
avatarThumb: res.data.data.avatarThumb,
gender: res.data.data.gender,
phoneNum: res.data.data.phoneNum,
email: res.data.data.email
}
})
}
if (messageData.sessionList[sessionId].sessionType === MsgType.GROUP_CHAT) {
const groupId = selectedSession.value.remoteId
groupData.setOneOfGroupMembers({
groupId: groupId,
account: account,
userInfo: {
...groupMembers.value[account],
nickName: res.data.data.nickName,
avatar: res.data.data.avatar,
avatarThumb: res.data.data.avatarThumb
}
})
}
})
.finally(() => {
//防止请求异常导致loading关不掉
loadingInstance.close()
})
}
}
const showMenuSessionId = ref('') //当前被点击右键的sessionId它可以不是选中的
const selectedMenuItem = ref('') //菜单组件反馈用户点击的某个菜单项
const isShowUpdateMarkDialog = ref(false)
const titleForUpdateMark = ref('')
const onShowUpdateMarkDialog = () => {
isShowUpdateMarkDialog.value = true
const objectInfo = messageData.sessionList[showMenuSessionId.value].objectInfo
if (messageData.sessionList[showMenuSessionId.value].sessionType === MsgType.CHAT) {
titleForUpdateMark.value = `${objectInfo.nickName} ${objectInfo.account}`
} else if (messageData.sessionList[showMenuSessionId.value].sessionType === MsgType.GROUP_CHAT) {
titleForUpdateMark.value = `${objectInfo.groupName} ${objectInfo.groupId}`
}
}
const onUpdateMarkConfirm = (inputValue) => {
// 如果没有更改,不需要执行保存
if (inputValue !== messageData.sessionList[showMenuSessionId.value].mark) {
messageData.updateSession({
sessionId: showMenuSessionId.value,
mark: inputValue
})
}
isShowUpdateMarkDialog.value = false
}
const onShowGroupCard = ({ groupId }) => {
if (messageData.sessionList[groupId].leave) {
ElMessage.warning('您已离开该群或群已被解散')
return
}
const loadingInstance = ElLoading.service(el_loading_options)
groupInfoService({ groupId: groupId })
.then((res) => {
groupCardData.setOpened(groupId)
groupData.setGroupInfo({
groupId: groupId,
groupInfo: res.data.data.groupInfo || {}
})
groupData.setGroupMembers({
groupId: groupId,
members: res.data.data.members || {}
})
})
.finally(() => {
loadingInstance.close()
})
}
const onShowContactCard = (contactInfo) => {
userCardData.setUserInfo(contactInfo)
userCardData.setIsShow(true)
}
const onOpenSession = async ({ msgType, objectInfo }) => {
if (myAccount.value === objectInfo.account) {
console.log('暂不支持自己给自己发消息') //TODO
return
}
let sessionId
let remoteId
if (msgType === MsgType.CHAT) {
sessionId = combineId(myAccount.value, objectInfo.account)
remoteId = objectInfo.account
} else if (msgType === MsgType.GROUP_CHAT) {
sessionId = objectInfo.groupId
remoteId = objectInfo.groupId
} else {
return
}
if (messageData.sessionList[sessionId]) {
handleSelectedSession(sessionId)
} else {
const res = await msgChatCreateSessionService({
sessionId: sessionId,
remoteId: remoteId,
sessionType: msgType
})
messageData.addSession(res.data.data.session)
handleSelectedSession(sessionId)
}
}
/**
* 监视msgIdsShow的数据变化,给出新消息的tips提示
*/
watch(
() => msgIdsShow.value,
(newValue) => {
if (!newValue || selectedSession.value.unreadCount === 0) return
nextTick(() => {
const unreadMsgEls = document.querySelectorAll('.unreadMsg')
if (unreadMsgEls.length === 0) return
const msgListRect = msgListDiv.value.getBoundingClientRect()
Array.from(unreadMsgEls).some((el) => {
const rect = el.getBoundingClientRect()
if (rect.bottom < msgListRect.top) {
newMsgTips.value.isShowTopTips = true
newMsgTips.value.unreadCount = selectedSession.value.unreadCount
newMsgTips.value.firstElement = el
return true
} else if (rect.top > msgListRect.bottom) {
newMsgTips.value.isShowBottomTips = true
newMsgTips.value.unreadCount = selectedSession.value.unreadCount
return true
}
})
})
}
)
watch(
() => router.currentRoute.value.query.sessionId,
(newValue) => {
if (newValue) {
handleSelectedSession(newValue)
}
}
)
const sessionItemRefCollection = ref({})
const setSessionItemRef = (sessionId, el) => {
sessionItemRefCollection.value[sessionId] = el
}
const onSelectMenu = (item) => {
selectedMenuItem.value = item
nextTick(() => {
// 要延后执行否则selectedMenuItem的值还没有传过去点击无效
sessionItemRefCollection.value[showMenuSessionId.value].handleSelectedMenuItem()
})
}
const onOpenSessionMenu = (sessionId) => {
showMenuSessionId.value = sessionId
}
const onCloseSessionMenu = () => {
showMenuSessionId.value = ''
}
const onNoneSelected = () => {
messageData.setSelectedSessionId('')
}
const onVoiceCall = () => {
ElMessage.warning('功能开发中')
}
const onVideoCall = () => {
ElMessage.warning('功能开发中')
}
const onInviteToGroup = () => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
const groupId = selectedSession.value.remoteId
const joinGroupApproval = groupData.groupInfoList[groupId].joinGroupApproval
if (joinGroupApproval || iAmAdmin.value) {
onShowGroupCard({ groupId: selectedSession.value.remoteId })
setTimeout(() => {
groupCardData.setChangeMemberModel('addMember')
}, 300)
} else {
ElMessage.warning('没有权限,请联系群组管理员')
}
} else if (selectedSession.value.sessionType === MsgType.CHAT) {
defaultSelectedOptionIds.value = [selectedSession.value.remoteId]
isShowSelectDialog.value = true
}
}
const iAmAdmin = computed(() => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
return groupMembers.value[myAccount.value].role > 0
} else {
return false
}
})
const onMoreSetting = () => {
if (selectedSession.value.sessionType === MsgType.CHAT) {
onShowUserCard({
sessionId: selectedSession.value.sessionId,
account: selectedSession.value.objectInfo.account
})
} else if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
onShowGroupCard({ groupId: selectedSession.value.remoteId })
}
}
const isShowSelectDialog = ref(false)
const addOprMenuRef = ref()
const onSelectOprMenu = (label) => {
switch (label) {
case 'createGroup':
defaultSelectedOptionIds.value = []
isShowSelectDialog.value = true
break
case 'createVoiceMeeting':
ElMessage.warning('功能开发中')
break
case 'createVideoMeeting':
ElMessage.warning('功能开发中')
break
default:
break
}
}
const showAddOprMenu = (e) => {
addOprMenuRef.value.handleSessionMenu(e)
}
/**
* 用于显示创建群组弹窗中的候选成员名单
*/
const selectDialogOptions = computed(() => {
const data = {}
Object.values(messageData.sessionList).forEach((item) => {
if (item.sessionType === MsgType.CHAT) {
data[item.objectInfo.account] = item.objectInfo
}
})
return data
})
/**
* 用于显示创建群组弹窗中的默认选中的名单idaccount
*/
const defaultSelectedOptionIds = ref([])
const onConfirmSelect = async (selected) => {
if (selected.length < 2) {
ElMessage.warning('请至少选择两位群成员')
return
}
const members = selected.map((item) => ({ account: item.account, nickName: item.nickName }))
members.push({ account: userData.user.account, nickName: userData.user.nickName })
const res = await groupCreateService({
groupName: `${userData.user.nickName}${selected[0].nickName}${selected[1].nickName}等的群组`,
groupType: 1, //普通群
members: members
})
groupData.setGroupInfo({
groupId: res.data.data.groupInfo.groupId,
groupInfo: res.data.data.groupInfo
})
const sessionId = res.data.data.groupInfo.groupId
const loadingInstance = ElLoading.service(el_loading_options)
new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => {
reject()
}, 3000)
watch(
() => messageData.sessionList[sessionId],
(newValue) => {
if (newValue) {
clearTimeout(timeoutId)
resolve()
}
}
)
})
.then(() => {
handleSelectedSession(sessionId)
})
.finally(() => {
isShowSelectDialog.value = false
loadingInstance.close()
})
}
const inputEditorRef = ref()
const onSendEmoji = (key) => {
inputEditorRef.value.addEmoji(key)
}
const onSendImage = ({ objectId }) => {
handleSendMessage(JSON.stringify({ type: msgContentType.IMAGE, value: objectId }))
}
const onSendAudio = ({ objectId }) => {
handleSendMessage(JSON.stringify({ type: msgContentType.AUDIO, value: objectId }))
}
const onSendRecording = ({ objectId }) => {
handleSendMessage(JSON.stringify({ type: msgContentType.RECORDING, value: objectId }))
}
const onSendVideo = ({ objectId }) => {
handleSendMessage(JSON.stringify({ type: msgContentType.VIDEO, value: objectId }))
}
const onSendDocument = ({ objectId }) => {
handleSendMessage(JSON.stringify({ type: msgContentType.DOCUMENT, value: objectId }))
}
const inputRecorderRef = ref(null)
const isShowRecorder = ref(false)
const onShowRecorder = () => {
isShowRecorder.value = true
}
</script>
<template>
<el-container class="msg-container-hole" @click="onClickMsgContainer">
<el-aside class="msg-aside bdr-r" :style="{ width: asideWidth + 'px' }">
<div class="msg-aside-main">
<div class="header bdr-b">
<SearchBox
@showContactCard="onShowContactCard"
@showGroupCard="onShowGroupCard"
@openSession="onOpenSession"
></SearchBox>
<AddOprMenu ref="addOprMenuRef" @selectMenu="onSelectOprMenu">
<AddButton :size="30" @click="showAddOprMenu($event)"></AddButton>
</AddOprMenu>
</div>
<SessionMenu
:sessionId="showMenuSessionId"
@selectMenu="onSelectMenu"
@closeMenu="onCloseSessionMenu"
>
<div class="session-list my-scrollbar" ref="sessionListRef">
<SessionItem
:ref="(el) => setSessionItemRef(item.sessionId, el)"
:id="`session-item-${sessionIdConvert(item.sessionId)}`"
v-for="item in sessionListSorted"
:key="item.sessionId"
:sessionId="item.sessionId"
:selectedSessionId="selectedSessionId"
:showMenuSessionId="showMenuSessionId"
:selectedMenuItem="selectedMenuItem"
@isSelected="handleSelectedSession"
@showUserCard="onShowUserCard"
@showGroupCard="onShowGroupCard"
@openSessionMenu="onOpenSessionMenu"
@noneSelected="onNoneSelected"
@showUpdateMarkDialog="onShowUpdateMarkDialog"
></SessionItem>
<HashNoData
v-if="sessionListSorted.length === 0"
:size="100"
style="height: 100%"
></HashNoData>
</div>
</SessionMenu>
</div>
<DragLine
direction="right"
:min="asideWidthMin"
:max="asideWidthMax"
:origin-size="asideWidth"
@drag-update="onAsideDragUpdate"
></DragLine>
</el-aside>
<el-main class="msg-box">
<div v-if="!selectedSessionId" class="backgroup">
<backgroupImage class="backgroup-image" v-if="!selectedSessionId"></backgroupImage>
<span class="welcome">欢迎使用 Open AnyLink</span>
</div>
<el-container v-else class="container">
<el-header class="header bdr-b">
<div class="show-name-id" @click="onMoreSetting">
<SessionTag
v-if="selectedSession?.sessionType === MsgType.GROUP_CHAT"
tagType="groupchat"
></SessionTag>
<SessionTag v-if="isNotInGroup" tagType="groupleave"></SessionTag>
<span
class="show-name text-ellipsis"
:title="selectedSession.mark ? `${selectedSession.mark}(${showName})` : showName"
>
{{ selectedSession.mark ? `${selectedSession.mark}(${showName})` : showName }}
</span>
<span class="show-id" :title="showId">{{ showId }}</span>
</div>
<div v-if="!isNotInGroup" class="action-set">
<el-icon
class="action-button"
size="20"
color="#409eff"
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '多人语音' : '语音通话'"
@click="onVoiceCall"
>
<Phone />
</el-icon>
<el-icon
class="action-button"
size="20"
color="#409eff"
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '视频会议' : '视频通话'"
@click="onVideoCall"
>
<VideoCamera />
</el-icon>
<el-icon
class="action-button"
size="20"
color="#409eff"
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '邀请进群' : '创建群组'"
@click="onInviteToGroup"
>
<CirclePlus />
</el-icon>
<el-icon
class="action-button"
size="20"
color="#409eff"
title="更多设置"
@click="onMoreSetting"
>
<MoreFilled />
</el-icon>
</div>
</el-header>
<el-main class="body">
<div class="show-main">
<div class="show-message-box">
<div v-if="selectedSessionCache[selectedSessionId]?.isLoading" class="show-loading">
数据加载中
</div>
<div v-else-if="!lastMsgId" class="no-more-message">当前无更多消息</div>
<div
v-else
class="message-main my-scrollbar"
ref="msgListDiv"
@wheel="handleMsgListWheel"
>
<MessageItem
v-for="item in msgIdsShow"
:key="selectedSessionId + '-' + item"
:sessionId="selectedSessionId"
:msgId="item"
:extend="msgExtend[item]"
:obj="getMsgSenderObj(item)"
:readMsgId="selectedSession.readMsgId"
:remoteRead="selectedSession.remoteRead"
:firstMsgId="firstMsgId"
:lastMsgId="lastMsgId"
:hasNoMoreMsg="hasNoMoreMsg"
:isLoadMoreLoading="selectedSessionCache[selectedSessionId]?.isLoadMoreLoading"
@loadMore="onLoadMore"
@showUserCard="onShowUserCard"
@showGroupCard="onShowGroupCard"
@resendMsg="handleResendMessage"
@loadFinished="updateScroll"
></MessageItem>
</div>
<el-button
type="primary"
class="return-bottom"
:class="{ showIt: isShowReturnBottom }"
@click="onReturnBottom"
>
返回底部
<el-icon class="el-icon--right"><ArrowDownBold /></el-icon>
</el-button>
<el-button
type="primary"
class="bottom-tips"
:class="{ showIt: newMsgTips.isShowBottomTips }"
@click="onReturnBottom"
>
{{ newMsgTips.unreadCount > 99 ? `99+` : newMsgTips.unreadCount }}条未读消息
<el-icon class="el-icon--right"><ArrowDownBold /></el-icon>
</el-button>
<el-button
type="primary"
class="top-tips"
:class="{ showIt: newMsgTips.isShowTopTips }"
@click="onReachFirstUnReadMsg"
>
{{ newMsgTips.unreadCount > 99 ? `99+` : newMsgTips.unreadCount }}条未读消息
<el-icon class="el-icon--right"><ArrowUp /></el-icon>
</el-button>
</div>
<div class="input-box bdr-t" :style="{ height: inputBoxHeight + 'px' }">
<el-container v-if="isShowRecorder">
<InputRecorder
ref="inputRecorderRef"
:sessionId="selectedSessionId"
@exit="isShowRecorder = false"
@sendRecording="onSendRecording"
></InputRecorder>
</el-container>
<el-container v-else class="input-box-container">
<el-header class="input-box-header">
<InputToolBar
ref="inputToolBarRef"
:sessionId="selectedSessionId"
:isShowToolSet="!isNotInGroup"
@sendEmoji="onSendEmoji"
@sendImage="onSendImage"
@sendAudio="onSendAudio"
@sendVideo="onSendVideo"
@sendDocument="onSendDocument"
@showRecorder="onShowRecorder"
></InputToolBar>
</el-header>
<el-main class="input-box-main">
<div
v-if="isNotInGroup"
style="
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: gray;
user-select: text;
"
>
您已离开该群或群已被解散
</div>
<InputEditor
v-else
ref="inputEditorRef"
:sessionId="selectedSessionId"
:draft="selectedSession.draft || ''"
@sendMessage="handleSendMessage"
></InputEditor>
</el-main>
</el-container>
<DragLine
direction="top"
:min="inputBoxHeightMin"
:max="inputBoxHeightMax"
:origin-size="inputBoxHeight"
@drag-update="onInputBoxDragUpdate"
></DragLine>
</div>
</div>
<MessageGroupRightSide
:sessionId="selectedSessionId"
@showGroupCard="onShowGroupCard"
@openSession="onOpenSession"
></MessageGroupRightSide>
</el-main>
</el-container>
</el-main>
</el-container>
<EditDialog
:isShow="isShowUpdateMarkDialog"
:title="'修改备注:'"
:titleExt="titleForUpdateMark"
:placeholder="'请输入备注'"
:defaultInput="messageData.sessionList[showMenuSessionId]?.mark || ''"
@close="isShowUpdateMarkDialog = false"
@confirm="onUpdateMarkConfirm"
></EditDialog>
<SelectUserDialog
v-model="isShowSelectDialog"
:options="selectDialogOptions"
:defaultSelected="defaultSelectedOptionIds"
:searchModel="'server'"
@showUserCard="onShowUserCard"
@confirm="onConfirmSelect"
>
<template #title>
<div style="font-size: 16px; font-weight: bold; white-space: nowrap">创建群组</div>
</template>
</SelectUserDialog>
</template>
<style lang="scss" scoped>
.msg-container-hole {
height: 100%;
user-select: none;
.msg-aside {
height: 100%;
position: relative;
.msg-aside-main {
width: 100%;
height: 100%;
display: flex; // 需要flex布局否则session-list的滚动条会有问题
flex-direction: column;
overflow: hidden; // 禁用它的滚动条
.header {
padding: 10px 10px 9px 0;
display: flex;
align-items: center;
}
.context-menu-container {
height: 100%;
overflow: hidden;
.session-list {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 0; // 防止右键点击到两个sessionItem中间的真空地带造成弹出的菜单不能准确找到到session
overflow-y: scroll;
}
}
}
}
.msg-box {
padding: 0;
display: flex;
justify-content: center;
overflow: hidden; // 禁用它的滚动条
.backgroup {
display: flex;
flex-direction: column;
justify-content: center;
.backgroup-image {
width: 600px;
height: 400px;
}
.welcome {
text-align: center;
color: #409eff;
font-size: 40px;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
}
.container {
width: 100%;
height: 100%;
.header {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
.show-name-id {
display: flex;
justify-content: center;
align-items: center;
user-select: text;
cursor: pointer;
.show-name {
max-width: 300px;
font-size: 16px;
font-weight: bold;
}
.show-id {
margin-left: 10px;
font-size: 14px;
color: gray;
}
}
.action-set {
min-width: 200px;
.action-button {
padding: 8px;
margin-left: 10px;
border-radius: 50%;
background-color: #fff;
border: transparent solid 1px;
cursor: pointer;
&:hover {
border: #409eff solid 1px;
color: #409eff;
}
}
}
}
.body {
width: 100%;
height: 100%;
padding: 0;
display: flex;
.show-main {
display: flex;
flex-direction: column;
flex: 1;
.show-message-box {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
.show-loading {
width: 100%;
height: 30px;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
color: #409eff;
font-size: 14px;
}
.no-more-message {
width: 100%;
height: 30px;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: gray;
user-select: text;
}
.message-main {
width: 100%;
padding: 10px;
overflow-y: scroll; // 用它的滚动条
}
.return-bottom {
position: absolute;
left: 0px;
bottom: -40px;
transition: bottom 1s ease-in-out;
&.showIt {
bottom: -2px;
}
}
.bottom-tips {
position: absolute;
right: 0%;
bottom: -40px;
transition: bottom 1s ease-in-out;
&.showIt {
bottom: -2px;
}
}
.top-tips {
position: absolute;
right: 0%;
top: -40px;
transition: top 1s ease-in-out;
&.showIt {
top: -2px;
}
}
}
.input-box {
width: 100%;
display: flex;
position: relative;
.input-box-header {
width: 100%;
height: auto;
padding: 0;
position: relative;
}
.input-box-main {
width: 100%;
padding: 0;
}
}
}
}
}
}
}
</style>