Files
open-anylink-web/src/views/message/MessageLayout.vue

1302 lines
40 KiB
Vue
Raw Normal View History

2024-10-28 17:08:16 +08:00
eslint-disable prettier/prettier
<script setup>
import { ref, onMounted, computed, nextTick, watch } from 'vue'
2024-10-28 17:08:16 +08:00
import {
Microphone,
2024-10-28 17:08:16 +08:00
VideoCamera,
MoreFilled,
2024-09-05 10:49:44 +08:00
CirclePlus,
2024-10-28 17:08:16 +08:00
LocationInformation,
Clock,
2024-09-05 10:49:44 +08:00
Picture,
FolderAdd,
CreditCard,
2024-09-24 19:53:36 +08:00
PictureRounded,
2024-09-27 21:21:23 +08:00
ArrowDownBold,
ArrowUp
2024-09-05 10:49:44 +08:00
} from '@element-plus/icons-vue'
import DragLine from '@/components/common/DragLine.vue'
2024-10-15 10:30:25 +08:00
import SearchBox from '@/components/search/SearchBox.vue'
2024-10-30 21:10:49 +08:00
import AddButton from '@/components/common/AddButton.vue'
2024-11-08 10:15:25 +08:00
import SessionItem from '@/views/message/components/SessionItem.vue'
import InputTool from '@/views/message/components/InputTool.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 SelectDialog from '@/components/common/SelectDialog.vue'
import {
userStore,
settingStore,
messageStore,
userCardStore,
groupCardStore,
groupStore
} from '@/stores'
2024-09-05 10:49:44 +08:00
import backgroupImage from '@/assets/messagebx_bg.webp'
import {
msgChatPullMsgService,
msgChatCreateSessionService,
msgChatQuerySessionService
} from '@/api/message'
import { groupInfoService, groupCreateService } from '@/api/group'
2024-09-14 22:36:16 +08:00
import { MsgType } from '@/proto/msg'
import wsConnect from '@/js/websocket/wsConnect'
2024-11-22 17:00:52 +08:00
import {
onReceiveChatMsg,
onReceiveChatReadMsg,
onReceiveGroupChatMsg,
onReceiveGroupChatReadMsg
} from '@/js/event'
import { userQueryService } from '@/api/user'
import { ElLoading, ElMessage } from 'element-plus'
import { el_loading_options } from '@/const/commonConst'
2024-10-24 16:46:24 +08:00
import { combineId, sessionIdConvert } from '@/js/utils/common'
2024-11-08 10:15:25 +08:00
import SessionMenu from '@/views/message/components/SessionMenu.vue'
import router from '@/router'
2024-10-30 11:31:25 +08:00
import { BEGIN_MSG_ID } from '@/const/msgConst'
import EditDialog from '@/components/common/EditDialog.vue'
import AddOprMenu from './components/AddOprMenu.vue'
2024-08-23 23:05:57 +08:00
const userData = userStore()
2024-09-02 13:59:02 +08:00
const settingData = settingStore()
2024-09-05 10:49:44 +08:00
const messageData = messageStore()
const userCardData = userCardStore()
const groupCardData = groupCardStore()
const groupData = groupStore()
2024-10-17 14:40:45 +08:00
const selectedSessionId = ref('') //当前被选中的session
const sessionListRef = ref()
2024-09-05 10:49:44 +08:00
const asideWidth = ref(0)
2024-09-05 10:49:44 +08:00
const asideWidthMin = 200
const asideWidthMax = 500
const inputBoxHeight = ref(0)
const inputBoxHeightMin = 200
const inputBoxHeightMax = 500
2024-09-05 10:49:44 +08:00
const msgListDiv = ref()
2024-09-27 21:21:23 +08:00
const newMsgTips = ref({
isShowTopTips: false,
isShowBottomTips: false,
unreadCount: 0,
firstElement: null
})
const myAccount = computed(() => {
return userData.user.account
})
2024-10-30 11:31:25 +08:00
// 消息拉取是否结束
const pullMsgDone = computed(() => {
return selectedSession.value.pullMsgDone || false
})
// 是否是msgRecordsList为空
const noMsgRecords = computed(() => {
return (
!messageData.msgRecordsList[selectedSessionId.value] ||
messageData.msgRecordsList[selectedSessionId.value].length === 0
)
})
// msgRecordsList的第一条消息ID
const firstMsgId = computed(() => {
if (!noMsgRecords.value) {
return messageData.msgRecordsList[selectedSessionId.value][0].msgId
} else {
return 0
}
})
// 是否是没有更多消息了从服务器拉取结束了或者firstMsgId是BEGIN_MSG_ID
const hasNoMoreMsg = computed(() => {
2024-10-30 11:31:25 +08:00
return pullMsgDone.value || firstMsgId.value === BEGIN_MSG_ID
})
2024-11-26 16:57:48 +08:00
const isMutedInGroup = computed(() => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
const groupInfo = groupData.groupInfoList[selectedSession.value.remoteId]
const members = groupData.groupMembersList[selectedSession.value.remoteId]
2024-11-26 16:57:48 +08:00
const me = members[myAccount.value]
if (me.mutedMode === 1 || (groupInfo.allMuted && me.mutedMode !== 2)) {
return true
} else {
return false
}
} else {
return false
}
})
2024-09-24 19:53:36 +08:00
const isShowReturnBottom = ref(false)
// 留在该页面上的session状态缓存例如
// isLoading: 正在加载数据解释会话首次被打开时开场加载数据的loading场景
// isLoadMoreLoading: 是否加载更多中解释会话被打开后向上移动滚轮到顶出现“加载更多”字样继续滚动或者点击的loading场景
2024-10-30 11:31:25 +08:00
// 这些数据不能放在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(() => {
2024-10-17 14:40:45 +08:00
if (selectedSessionId.value) {
const len = messageData.msgRecordsList[selectedSessionId.value]?.length
return len > capacity.value ? len - capacity.value : 0
2024-10-28 17:08:16 +08:00
} else {
return 0
}
})
2024-09-20 12:11:32 +08:00
const initSession = (sessionId) => {
capacity.value = 15 //会话的默认显示消息记录数
msgListReachBottom(false) //会话默认滚到最底部
isShowReturnBottom.value = false //会话默认不弹出“返回底部”的按钮
// 如果selectedSessionCache有这个sessionId就不重置
if (!selectedSessionCache.value[sessionId]) {
selectedSessionCache.value[sessionId] = {
isLoading: false,
isLoadMoreLoading: false
}
}
}
const locateSession = (sessionId) => {
nextTick(() => {
2024-10-25 20:47:00 +08:00
const selectedElement = document.querySelector(`#session-item-${sessionIdConvert(sessionId)}`)
// 如果被选中元素的上边在scrollTop之的上面或这在下边在scrollTop+clientHeight的下面显示不全或者完全没有显示则需要重新定位
// 由于offsetTop和offsetHeight不包含外边距因此定位存在细小误差暂不处理
2024-10-16 13:35:03 +08:00
if (selectedElement.offsetTop - selectedElement.offsetHeight < sessionListRef.value.scrollTop) {
sessionListRef.value.scrollTop = selectedElement.offsetTop - selectedElement.offsetHeight
2024-10-28 17:08:16 +08:00
} else if (
selectedElement.offsetTop >
sessionListRef.value.scrollTop + sessionListRef.value.clientHeight
) {
2024-10-16 13:35:03 +08:00
sessionListRef.value.scrollTop = selectedElement.offsetTop - sessionListRef.value.clientHeight
}
})
}
2024-09-20 12:11:32 +08:00
const msgRecords = computed(() => {
2024-10-17 14:40:45 +08:00
return messageData.msgRecordsList[selectedSessionId.value]?.slice(startIndex.value)
2024-09-20 12:11:32 +08:00
})
2024-10-16 13:35:03 +08:00
const selectedSession = computed(() => {
2024-10-17 14:40:45 +08:00
return messageData.sessionList[selectedSessionId.value]
})
2024-09-05 17:41:27 +08:00
onMounted(async () => {
2024-11-06 16:59:41 +08:00
await messageData.loadSessionList()
await messageData.loadPartitions()
await groupData.loadGroupInfoList()
2024-11-06 16:59:41 +08:00
asideWidth.value = settingData.sessionListDrag[myAccount.value] || 300
inputBoxHeight.value = settingData.inputBoxDrag[myAccount.value] || 300
wsConnect.bindEvent(MsgType.CHAT, onReceiveChatMsg(selectedSessionId, msgListDiv, capacity)) //绑定接收Chat消息的事件
wsConnect.bindEvent(MsgType.CHAT_READ, onReceiveChatReadMsg()) //绑定接收Chat已读消息的事件
wsConnect.bindEvent(
MsgType.GROUP_CHAT,
onReceiveGroupChatMsg(selectedSessionId, msgListDiv, capacity)
) //绑定接收GroupChat消息的事件
2024-11-22 17:00:52 +08:00
wsConnect.bindEvent(MsgType.GROUP_CHAT_READ, onReceiveGroupChatReadMsg()) //绑定接收GroupChat已读消息的事件
2024-10-28 17:08:16 +08:00
// 定时更新单聊对象的状态
const accounts = []
2024-10-28 17:08:16 +08:00
Object.keys(messageData.sessionList).forEach((key) => {
const session = messageData.sessionList[key]
const sessionType = session.sessionType
2024-10-28 17:08:16 +08:00
if (sessionType === MsgType.CHAT) {
//只看单聊的,群里在打开聊天窗时触发查询
accounts.push(session.objectInfo.account)
}
})
2024-10-28 17:08:16 +08:00
setInterval(() => {
wsConnect.statusReq(JSON.stringify(accounts))
}, 5000)
// 这里要接收从其他页面跳转过来传递的sessionId参数
const routerSessionId = router.currentRoute.value.query.sessionId
if (routerSessionId) {
if (routerSessionId in messageData.sessionList) {
handleSelectedSession(routerSessionId)
} else {
const res = await msgChatQuerySessionService({ sessionId: routerSessionId })
if (res.data.data) {
messageData.addSession(res.data.data)
handleSelectedSession(routerSessionId)
}
}
}
})
2024-09-27 21:21:23 +08:00
const handleMsgListWheel = async () => {
if (
msgListDiv.value.scrollTop === 0 &&
!selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading
) {
await onLoadMore()
}
2024-09-24 19:53:36 +08:00
2024-09-27 21:21:23 +08:00
const clientHeight = document.querySelector('.message-main').clientHeight
const diffToBottom = msgListDiv.value.scrollHeight - msgListDiv.value.scrollTop - clientHeight
// diffToBottom接近50个像素的时候关闭底部未读tips控件
2024-09-27 21:21:23 +08:00
newMsgTips.value.isShowBottomTips = diffToBottom < 50 ? false : newMsgTips.value.isShowBottomTips
// isShowReturnBottom.value = diffToBottom > 300 // 控制是否显示"回到底部"的按钮。暂时取消这个提示功能,与消息提示的按钮显得有点重复
2024-09-27 21:21:23 +08:00
if (newMsgTips.value.firstElement?.getBoundingClientRect().top > 0) {
newMsgTips.value.isShowTopTips = false
}
}
2024-09-14 18:03:40 +08:00
// 把sessionList转成数组并按照lastMsgTime排序
const sessionListSorted = computed(() => {
2024-10-24 11:44:37 +08:00
if (!Object.keys(messageData.sessionList)) {
return []
2024-10-28 17:08:16 +08:00
} else {
let sessionArr = Object.values(messageData.sessionList)
2024-10-10 21:28:30 +08:00
return sessionArr.sort((a, b) => {
2024-10-28 17:08:16 +08:00
if (a.top && !b.top) {
// 排序第一优先级:是否置顶
2024-10-10 21:28:30 +08:00
return -1
2024-10-28 17:08:16 +08:00
} else if (!a.top && b.top) {
2024-10-10 21:28:30 +08:00
return 1
2024-10-28 17:08:16 +08:00
} else {
if (a.draft && !b.draft) {
// 排序第二优先级:是否有草稿
2024-10-10 21:28:30 +08:00
return -1
2024-10-28 17:08:16 +08:00
} else if (!a.draft && b.draft) {
2024-10-10 21:28:30 +08:00
return 1
2024-10-28 17:08:16 +08:00
} else {
2024-10-10 21:28:30 +08:00
// 排序第三优先级:最后一条消息的时间
const bTime = new Date(b.lastMsgTime).getTime()
const aTIme = new Date(a.lastMsgTime).getTime()
if (bTime !== aTIme) {
return bTime - aTIme
}
2024-10-10 21:28:30 +08:00
}
}
})
}
2024-09-05 10:49:44 +08:00
})
2024-09-05 23:02:44 +08:00
const showName = computed(() => {
2024-10-16 13:35:03 +08:00
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 ||
'没有群名称'
)
2024-09-05 23:02:44 +08:00
default:
return ''
}
})
const showId = computed(() => {
return selectedSession.value.remoteId
2024-09-05 23:02:44 +08:00
})
2024-10-30 11:31:25 +08:00
const getPreMsgTime = (index) => {
if (index > 0) {
2024-10-28 17:08:16 +08:00
return msgRecords.value[index - 1].msgTime
} else {
2024-10-28 17:08:16 +08:00
return null
}
}
2024-11-22 10:51:27 +08:00
const getMsgSenderObj = (item) => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
// 如果此时memberList还没有加载完成先return account给MessageItem子组件
const memberList = groupData.groupMembersList[selectedSession.value.remoteId]
return memberList ? memberList[item.fromId] : { account: item.fromId }
2024-11-22 10:51:27 +08:00
} else {
if (myAccount.value === item.fromId) {
2024-11-22 10:51:27 +08:00
return userData.user
} else {
return selectedSession.value.objectInfo
}
}
}
2024-10-30 12:59:01 +08:00
const lastReadMsgId = ref()
2024-09-27 21:21:23 +08:00
/**
2024-09-29 11:49:16 +08:00
* 判断是否是打开session后的第一条未读消息
2024-10-28 17:08:16 +08:00
* @param index
2024-09-27 21:21:23 +08:00
*/
const isFirstNew = (index) => {
2024-10-28 17:08:16 +08:00
if (index > 0 && msgRecords.value[index - 1].msgId == lastReadMsgId.value) {
2024-09-27 21:21:23 +08:00
return true
}
2024-10-28 17:08:16 +08:00
return false
2024-09-27 21:21:23 +08:00
}
2024-09-05 10:49:44 +08:00
const onAsideDragUpdate = ({ width }) => {
asideWidth.value = width
settingData.setSessionListDrag({
...settingData.sessionListDrag,
[myAccount.value]: width
})
}
2024-09-03 17:57:54 +08:00
2024-09-05 10:49:44 +08:00
const onInputBoxDragUpdate = ({ height }) => {
inputBoxHeight.value = height
msgListReachBottom()
settingData.setInputBoxDrag({
...settingData.inputBoxDrag,
[myAccount.value]: height
})
2024-09-05 10:49:44 +08:00
}
2024-09-25 23:46:30 +08:00
/**
* 通过REST接口主动拉取消息
* @param mode 0:拉取最近N条;1:按refMsgId向上拉取N条(上滑加载更多消息)
* @param ref mode=1时要携带,标记更新的msgId位置
*/
const pullMsg = async (mode = 0, ref = -1) => {
// 下列三种情况不拉取数据
if (
hasNoMoreMsg.value ||
selectedSessionCache.value[selectedSessionId.value].isLoading ||
selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading
) {
return
}
2024-10-28 17:08:16 +08:00
const pageSize = 30
const params = {
2024-10-28 17:08:16 +08:00
sessionId: selectedSessionId.value,
pageSize: pageSize,
mode: mode,
refMsgId: ref
}
// mode=0才需要显示"数据加载中..."
if (mode === 0) selectedSessionCache.value[selectedSessionId.value].isLoading = true
// mode=1才需要显示"加载更多中..."
if (mode === 1) 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) {
messageData.addMsgRecords(sessionId, res.data.data.msgList)
if (mode === 0) {
messageData.updateSession({
sessionId: sessionId,
lastMsgId: res.data.data.lastMsgId,
2024-11-27 17:12:14 +08:00
lastMsgType: res.data.data.msgList[msgCount - 1].msgType,
lastMsgContent: res.data.data.msgList[msgCount - 1].content,
lastMsgAccount: res.data.data.msgList[msgCount - 1].fromId,
lastMsgTime: res.data.data.msgList[msgCount - 1].msgTime
})
}
}
if (msgCount < pageSize) {
messageData.updateSession({
sessionId: sessionId,
2024-10-30 11:31:25 +08:00
pullMsgDone: true
})
}
} finally {
if (mode === 0) selectedSessionCache.value[selectedSessionId.value].isLoading = false
if (mode === 1) selectedSessionCache.value[selectedSessionId.value].isLoadMoreLoading = false
}
}
// 表示有个session被选中了
2024-10-25 23:38:53 +08:00
const handleSelectedSession = async (sessionId) => {
2024-10-17 14:40:45 +08:00
if (selectedSessionId.value !== sessionId) {
selectedSessionId.value = sessionId
router.push({
path: '/message',
query: {
sessionId: sessionId
}
})
initSession(sessionId)
2024-10-17 14:40:45 +08:00
locateSession(sessionId)
2024-10-28 17:08:16 +08:00
2024-11-22 10:51:27 +08:00
// 如果是群组要加载成员列表显示消息需要accountnickNameavatar信息
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
const groupId = selectedSession.value.remoteId
2024-11-22 10:51:27 +08:00
// 没有members数据才需要加载成员列表加载过了就不重复加载了
if (!groupData.groupMembersList[groupId]) {
const res = await groupInfoService({ groupId: groupId })
2024-11-26 16:57:48 +08:00
groupData.setGroupInfo(res.data.data.groupInfo)
2024-11-22 10:51:27 +08:00
groupData.setGroupMembers({
groupId: groupId,
members: res.data.data.members
})
}
}
2024-09-26 10:28:00 +08:00
// 如果切换到的session在之前都没有pull过消息,则需要pull一次(mode=0),且lastMsgId有值才pull
2024-10-16 13:35:03 +08:00
if (!msgRecords.value && selectedSession.value.lastMsgId) {
2024-09-26 10:28:00 +08:00
await pullMsg()
msgListReachBottom()
}
2024-10-16 13:35:03 +08:00
lastReadMsgId.value = selectedSession.value.readMsgId //保存这个readMsgId,要留给MessageItem用
2024-09-26 10:28:00 +08:00
handleRead()
}
}
const handleRead = () => {
2024-10-28 17:08:16 +08:00
if (
selectedSessionId.value &&
selectedSession.value.readMsgId < selectedSession.value.lastMsgId
) {
2024-10-16 13:35:03 +08:00
const content = selectedSession.value.lastMsgId.toString()
2024-11-22 17:00:52 +08:00
const msgType =
selectedSession.value.sessionType === MsgType.CHAT
? MsgType.CHAT_READ
: MsgType.GROUP_CHAT_READ
wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', () => {})
2024-09-25 23:46:30 +08:00
// 更新本地缓存的已读位置
messageData.updateSession({
2024-10-28 17:08:16 +08:00
sessionId: selectedSessionId.value,
2024-09-25 23:46:30 +08:00
readMsgId: content,
readTime: new Date(),
unreadCount: 0
})
}
}
2024-09-05 23:02:44 +08:00
const handleSendMessage = (content) => {
2024-11-26 16:57:48 +08:00
if (isMutedInGroup.value) {
ElMessage.warning('您已被禁言,请联系管理员')
return
}
// TODO 这里还要考虑失败情况1消息发不出去2消息发出去了服务器不发“已发送”
2024-11-22 17:00:52 +08:00
wsConnect.sendMsg(
selectedSessionId.value,
showId.value,
selectedSession.value.sessionType,
content,
(msgId) => {
const now = new Date()
messageData.updateSession({
2024-10-28 17:08:16 +08:00
sessionId: selectedSessionId.value,
2024-11-22 17:00:52 +08:00
lastMsgId: msgId, // 最后一条消息(自己发的)
2024-11-27 17:12:14 +08:00
lastMsgType: selectedSession.value.sessionType,
2024-11-22 17:00:52 +08:00
lastMsgContent: content,
lastMsgAccount: myAccount.value,
2024-11-22 17:00:52 +08:00
lastMsgTime: now,
readMsgId: msgId, // 最后一条消息是自己发的因此已读更新到刚发的这条消息的msgId
readTime: now,
unreadCount: 0, // 最后一条消息是自己发的因此未读是0
draft: '' //草稿意味着要清空
})
2024-11-22 17:00:52 +08:00
messageData.addMsgRecords(selectedSessionId.value, [
{
sessionId: selectedSessionId.value,
msgId: msgId,
fromId: myAccount.value,
2024-11-22 17:00:52 +08:00
msgType: selectedSession.value.sessionType,
content: content,
msgTime: now
}
])
msgListReachBottom(false) // 发送消息之后,msgList要触底
}
)
2024-09-14 18:03:40 +08:00
}
const onLoadMore = async () => {
const scrollHeight = msgListDiv.value.scrollHeight
const scrollTop = msgListDiv.value.scrollTop
if (messageData.msgRecordsList[selectedSessionId.value]?.length <= capacity.value) {
await pullMsg(1, msgRecords.value[0].msgId)
}
2024-10-17 14:40:45 +08:00
const len = messageData.msgRecordsList[selectedSessionId.value]?.length
if (len > capacity.value) {
if (len - capacity.value > step) {
capacity.value += step
2024-10-28 17:08:16 +08:00
} else {
capacity.value = len
}
}
// 保持页面对话的锚定位置
nextTick(() => {
msgListDiv.value.scrollTop = msgListDiv.value.scrollHeight - scrollHeight + scrollTop
2024-10-28 17:08:16 +08:00
})
}
2024-09-08 23:49:43 +08:00
2024-10-30 11:31:25 +08:00
// 消息列表拉到最底部
const msgListReachBottom = (isSmooth = true) => {
const behavior = isSmooth ? 'smooth' : 'instant'
2024-09-14 18:03:40 +08:00
nextTick(() => {
2024-09-25 21:50:24 +08:00
msgListDiv.value?.scrollTo({
2024-09-14 22:36:16 +08:00
top: msgListDiv.value.scrollHeight,
2024-09-26 10:28:00 +08:00
behavior: behavior
2024-09-14 18:03:40 +08:00
})
})
2024-09-27 21:21:23 +08:00
newMsgTips.value.isShowBottomTips = false
}
2024-09-14 18:03:40 +08:00
2024-09-24 19:53:36 +08:00
const onReturnBottom = () => {
msgListReachBottom()
}
const onReachFirstUnReadMsg = () => {
2024-10-28 17:08:16 +08:00
const msgListRect = msgListDiv.value.getBoundingClientRect()
2024-09-27 21:21:23 +08:00
const firstElRect = newMsgTips.value.firstElement.getBoundingClientRect()
nextTick(() => {
msgListDiv.value.scrollTop = msgListDiv.value.scrollTop - (msgListRect.top - firstElRect.top)
2024-10-28 17:08:16 +08:00
})
2024-09-27 21:21:23 +08:00
newMsgTips.value.isShowTopTips = false
}
const onClickMsgContainer = () => {
handleRead()
}
const onShowUserCard = async ({ sessionId, account }) => {
const loadingInstance = ElLoading.service(el_loading_options)
if (myAccount.value === account) {
2024-10-28 17:08:16 +08:00
userData
.updateUser()
.then(() => {
userCardData.setUserInfo(userData.user)
userCardData.setIsShow(true)
2024-10-28 17:08:16 +08:00
})
.finally(() => {
//防止请求异常导致loading关不掉
loadingInstance.close()
})
} else {
2024-10-28 10:00:05 +08:00
userQueryService({ account: account })
2024-10-28 17:08:16 +08:00
.then((res) => {
2024-11-22 17:00:52 +08:00
userCardData.setUserInfo(res.data.data)
userCardData.setIsShow(true)
2024-11-22 17:00:52 +08:00
// 如果是单聊需要更新session中的objectInfo. 因为群成员有可能不在sessionList中所以不更新
if (messageData.sessionList[sessionId].sessionType === MsgType.CHAT) {
messageData.updateSession({
sessionId: sessionId,
objectInfo: {
...messageData.sessionList[sessionId].objectInfo,
nickName: res.data.data.nickName,
signature: res.data.data.signature,
avatarThumb: res.data.data.avatarThumb,
gender: res.data.data.gender,
phoneNum: res.data.data.phoneNum,
email: res.data.data.email
}
})
}
2024-10-28 17:08:16 +08:00
})
.finally(() => {
//防止请求异常导致loading关不掉
loadingInstance.close()
2024-10-28 10:00:05 +08:00
})
}
}
const showMenuSessionId = ref('') //当前被点击右键的sessionId它可以不是选中的
const selectedMenuItem = ref('') //菜单组件反馈用户点击的某个菜单项
const isShowUpdateMarkDialog = ref(false)
2024-11-21 16:46:27 +08:00
const titleForUpdateMark = ref('')
const onShowUpdateMarkDialog = () => {
isShowUpdateMarkDialog.value = true
2024-11-21 16:46:27 +08:00
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({
2024-11-21 16:46:27 +08:00
sessionId: showMenuSessionId.value,
mark: inputValue
})
}
isShowUpdateMarkDialog.value = false
}
const onShowGroupCard = async ({ groupId }) => {
2024-11-21 16:46:27 +08:00
const loadingInstance = ElLoading.service(el_loading_options)
groupInfoService({ groupId: groupId })
.then((res) => {
groupCardData.setGroupId(groupId)
groupCardData.setIsShow(true)
groupData.setGroupInfo(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) {
2024-10-28 17:08:16 +08:00
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]) {
2024-10-25 23:38:53 +08:00
handleSelectedSession(sessionId)
2024-10-28 17:08:16 +08:00
} else {
const res = await msgChatCreateSessionService({
2024-10-28 17:08:16 +08:00
sessionId: sessionId,
remoteId: remoteId,
2024-10-28 17:08:16 +08:00
sessionType: msgType
})
messageData.addSession(res.data.data)
2024-10-25 23:38:53 +08:00
handleSelectedSession(sessionId)
}
}
2024-09-27 21:21:23 +08:00
/**
* 监视msgRecords的数据变化,给出新消息的tips提示
*/
2024-10-28 17:08:16 +08:00
watch(
() => msgRecords.value,
2024-11-28 16:39:30 +08:00
(newValue) => {
if (!newValue || selectedSession.value.unreadCount === 0) return
2024-10-28 17:08:16 +08:00
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
}
})
2024-09-27 21:21:23 +08:00
})
2024-10-28 17:08:16 +08:00
}
)
2024-09-27 21:21:23 +08:00
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()
})
}
2024-10-28 20:45:06 +08:00
const onOpenSessionMenu = (sessionId) => {
showMenuSessionId.value = sessionId
}
const onNoneSelected = () => {
selectedSessionId.value = ''
}
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 || iAmManager.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 iAmManager = computed(() => {
if (selectedSession.value.sessionType === MsgType.GROUP_CHAT) {
const groupId = selectedSession.value.remoteId
const members = groupData.groupMembersList[groupId]
return members[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(res.data.data.groupInfo)
isShowSelectDialog.value = false
// 所有成员拿到chat_session在群主创建群组的时候统一新增了所有这里只需要查询
msgChatQuerySessionService({
sessionId: res.data.data.groupInfo.groupId
}).then((res) => {
messageData.addSession(res.data.data)
2024-11-29 09:11:47 +08:00
handleSelectedSession(res.data.data.sessionId)
})
}
</script>
<template>
2024-09-27 21:21:23 +08:00
<el-container class="msg-container-hole" @click="onClickMsgContainer">
2024-09-02 13:59:02 +08:00
<el-aside class="msg-aside bdr-r" :style="{ width: asideWidth + 'px' }">
<div class="msg-aside-main">
2024-09-05 10:49:44 +08:00
<div class="header">
<SearchBox
@showContactCard="onShowContactCard"
@showGroupCard="onShowGroupCard"
@openSession="onOpenSession"
></SearchBox>
<AddOprMenu ref="addOprMenuRef" @selectMenu="onSelectOprMenu">
<AddButton :size="30" @click="showAddOprMenu($event)"></AddButton>
</AddOprMenu>
</div>
2024-10-28 20:45:06 +08:00
<SessionMenu :sessionId="showMenuSessionId" @selectMenu="onSelectMenu">
<div class="session-list my-scrollbar" ref="sessionListRef">
2024-10-25 20:47:00 +08:00
<SessionItem
:ref="(el) => setSessionItemRef(item.sessionId, el)"
2024-10-25 20:47:00 +08:00
:id="`session-item-${sessionIdConvert(item.sessionId)}`"
v-for="item in sessionListSorted"
:key="item.sessionId"
:sessionId="item.sessionId"
2024-10-17 14:40:45 +08:00
:selectedSessionId="selectedSessionId"
:showMenuSessionId="showMenuSessionId"
:selectedMenuItem="selectedMenuItem"
2024-10-25 23:38:53 +08:00
@isSelected="handleSelectedSession"
@showUserCard="onShowUserCard"
@showGroupCard="onShowGroupCard"
2024-10-28 20:45:06 +08:00
@openSessionMenu="onOpenSessionMenu"
@noneSelected="onNoneSelected"
@showUpdateMarkDialog="onShowUpdateMarkDialog"
2024-10-25 20:47:00 +08:00
></SessionItem>
</div>
2024-10-28 20:45:06 +08:00
</SessionMenu>
</div>
<DragLine
direction="right"
2024-09-05 10:49:44 +08:00
:min="asideWidthMin"
:max="asideWidthMax"
:origin-size="asideWidth"
2024-09-05 10:49:44 +08:00
@drag-update="onAsideDragUpdate"
></DragLine>
</el-aside>
2024-09-05 10:49:44 +08:00
<el-main class="msg-box">
<el-image
class="backgroup-image"
2024-10-17 14:40:45 +08:00
v-if="!selectedSessionId"
2024-09-05 10:49:44 +08:00
:src="backgroupImage"
fit="cover"
></el-image>
<el-container v-else class="container">
2024-09-05 10:49:44 +08:00
<el-header class="header bdr-b">
2024-09-05 23:02:44 +08:00
<div class="show-name-id">
<SessionTag
v-if="selectedSession?.sessionType === MsgType.GROUP_CHAT"
tagType="groupchat"
></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>
2024-09-05 23:02:44 +08:00
</div>
2024-09-05 10:49:44 +08:00
<div class="action-set">
<el-icon
class="action-button"
size="20"
2024-11-25 20:57:36 +08:00
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '多人语音' : '语音通话'"
@click="onVoiceCall"
>
<Microphone />
</el-icon>
<el-icon
class="action-button"
size="20"
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '视频会议' : '视频通话'"
@click="onVideoCall"
>
<VideoCamera />
</el-icon>
<el-icon
class="action-button"
size="20"
:title="selectedSession.sessionType === MsgType.GROUP_CHAT ? '邀请进群' : '创建群组'"
@click="onInviteToGroup"
>
<CirclePlus />
</el-icon>
<el-icon class="action-button" size="20" title="更多设置" @click="onMoreSetting">
<MoreFilled />
</el-icon>
2024-09-05 10:49:44 +08:00
</div>
</el-header>
<el-main class="body">
2024-09-24 19:53:36 +08:00
<div class="show-box">
<div v-if="selectedSessionCache[selectedSessionId].isLoading" class="show-loading">
数据加载中
</div>
2024-10-30 11:31:25 +08:00
<div v-else-if="!selectedSession.lastMsgId" class="no-more-message">当前无更多消息</div>
2024-09-24 19:53:36 +08:00
<div
v-else
class="message-main my-scrollbar"
ref="msgListDiv"
2024-09-27 21:21:23 +08:00
@wheel="handleMsgListWheel"
2024-09-24 19:53:36 +08:00
>
<MessageItem
v-for="(item, index) in msgRecords"
:key="index"
2024-10-17 14:40:45 +08:00
:sessionId="selectedSessionId"
2024-09-14 22:36:16 +08:00
:msg="item"
2024-11-22 10:51:27 +08:00
:obj="getMsgSenderObj(item)"
:readMsgId="selectedSession.readMsgId"
2024-10-16 13:35:03 +08:00
:remoteRead="selectedSession.remoteRead"
2024-10-30 11:31:25 +08:00
:preMsgTime="getPreMsgTime(index)"
2024-09-27 21:21:23 +08:00
:isFirstNew="isFirstNew(index)"
:firstMsgId="firstMsgId"
:hasNoMoreMsg="hasNoMoreMsg"
:isLoadMoreLoading="selectedSessionCache[selectedSessionId].isLoadMoreLoading"
@loadMore="onLoadMore"
@showUserCard="onShowUserCard"
@showGroupCard="onShowGroupCard"
></MessageItem>
2024-09-08 23:49:43 +08:00
</div>
2024-09-24 19:53:36 +08:00
<el-button
type="primary"
class="return-bottom"
:class="{ showIt: isShowReturnBottom }"
@click="onReturnBottom"
>
返回底部
<el-icon class="el-icon--right"><ArrowDownBold /></el-icon>
2024-09-24 19:53:36 +08:00
</el-button>
2024-09-27 21:21:23 +08:00
<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>
2024-09-27 21:21:23 +08:00
</el-button>
<el-button
type="primary"
class="top-tips"
:class="{ showIt: newMsgTips.isShowTopTips }"
@click="onReachFirstUnReadMsg"
2024-09-27 21:21:23 +08:00
>
{{ newMsgTips.unreadCount > 99 ? `99+` : newMsgTips.unreadCount }}条未读消息
<el-icon class="el-icon--right"><ArrowUp /></el-icon>
2024-09-27 21:21:23 +08:00
</el-button>
2024-09-08 23:49:43 +08:00
</div>
2024-09-05 10:49:44 +08:00
<div class="input-box bdr-t" :style="{ height: inputBoxHeight + 'px' }">
<el-container class="input-box-container">
<el-header class="input-box-header">
<DragLine
direction="top"
:min="inputBoxHeightMin"
:max="inputBoxHeightMax"
:origin-size="inputBoxHeight"
@drag-update="onInputBoxDragUpdate"
></DragLine>
<div class="tool-set">
<div class="left-tools">
<InputTool tips="表情">
<template #iconSlot>
<PictureRounded />
</template>
</InputTool>
<InputTool tips="图片">
<template #iconSlot>
<Picture />
</template>
</InputTool>
<InputTool tips="文件">
<template #iconSlot>
<FolderAdd />
</template>
</InputTool>
<InputTool tips="代码">
<template #iconSlot>
<CreditCard />
</template>
</InputTool>
<InputTool tips="位置">
<template #iconSlot>
<LocationInformation />
</template>
</InputTool>
</div>
<div class="right-tools">
2024-10-12 00:01:09 +08:00
<InputTool tips="聊天记录">
2024-09-05 10:49:44 +08:00
<template #iconSlot>
<Clock />
</template>
</InputTool>
</div>
</div>
</el-header>
<el-main class="input-box-main">
<InputEditor
2024-10-17 14:40:45 +08:00
:sessionId="selectedSessionId"
2024-10-16 13:35:03 +08:00
:draft="selectedSession.draft || ''"
@sendMessage="handleSendMessage"
></InputEditor>
2024-09-05 10:49:44 +08:00
</el-main>
</el-container>
</div>
</el-main>
</el-container>
</el-main>
</el-container>
<EditDialog
:isShow="isShowUpdateMarkDialog"
:title="'修改备注:'"
2024-11-21 16:46:27 +08:00
:titleExt="titleForUpdateMark"
:placeholder="'请输入备注'"
:defaultInput="messageData.sessionList[showMenuSessionId]?.mark || ''"
@close="isShowUpdateMarkDialog = false"
@confirm="onUpdateMarkConfirm"
></EditDialog>
<SelectDialog
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>
</SelectDialog>
</template>
<style lang="scss" scoped>
.msg-container-hole {
height: 100%;
user-select: none;
.msg-aside {
height: 100%;
position: relative;
.msg-aside-main {
2024-09-05 17:41:27 +08:00
width: 100%;
height: 100%;
display: flex; // 需要flex布局否则session-list的滚动条会有问题
flex-direction: column;
2024-09-05 17:41:27 +08:00
overflow: hidden; // 禁用它的滚动条
.header {
2024-10-30 22:10:34 +08:00
padding: 10px 10px 10px 0;
2024-09-05 17:41:27 +08:00
display: flex;
2024-10-12 00:01:09 +08:00
align-items: center;
2024-09-05 17:41:27 +08:00
}
.context-menu-container {
overflow: hidden;
.session-list {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
2024-10-25 20:47:00 +08:00
gap: 0; // 防止右键点击到两个sessionItem中间的真空地带造成弹出的菜单不能准确找到到session
overflow-y: scroll;
}
2024-09-05 10:49:44 +08:00
}
}
}
.msg-box {
padding: 0;
2024-09-05 20:57:33 +08:00
overflow: hidden; // 禁用它的滚动条
2024-09-05 10:49:44 +08:00
.backgroup-image {
width: 100%;
height: 100%;
}
.container {
width: 100%;
height: 100%;
.header {
width: 100%;
2024-11-01 23:33:36 +08:00
height: 60px;
2024-09-05 10:49:44 +08:00
display: flex;
align-items: center;
justify-content: space-between;
2024-09-05 23:02:44 +08:00
.show-name-id {
display: flex;
justify-content: center;
2024-09-05 23:02:44 +08:00
align-items: center;
user-select: text;
.show-name {
max-width: 300px;
2024-09-05 23:02:44 +08:00
font-size: 16px;
font-weight: bold;
}
.show-id {
margin-left: 10px;
font-size: 14px;
color: gray;
}
2024-09-05 10:49:44 +08:00
}
.action-set {
.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;
}
2024-09-05 10:49:44 +08:00
}
}
}
.body {
width: 100%;
height: 100%;
padding: 0;
display: flex;
flex-direction: column;
2024-09-09 16:45:30 +08:00
overflow: hidden; // 禁用它的滚动条
position: relative;
2024-09-05 10:49:44 +08:00
.show-box {
width: 100%;
display: flex;
flex: 1;
2024-09-24 19:53:36 +08:00
overflow: hidden;
position: relative;
2024-09-08 23:49:43 +08:00
2024-10-30 11:31:25 +08:00
.show-loading {
width: 100%;
height: 30px;
2024-10-30 12:54:48 +08:00
padding: 20px;
2024-10-30 11:31:25 +08:00
display: flex;
justify-content: center;
align-items: center;
color: #409eff;
font-size: 14px;
}
.no-more-message {
width: 100%;
height: 30px;
2024-10-30 12:54:48 +08:00
padding: 20px;
2024-10-30 11:31:25 +08:00
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: gray;
user-select: text;
}
2024-09-08 23:49:43 +08:00
.message-main {
width: 100%;
2024-10-30 12:54:48 +08:00
padding: 10px;
2024-09-24 19:53:36 +08:00
overflow-y: scroll; // 用它的滚动条
}
.return-bottom {
position: absolute;
left: 0px;
bottom: -40px;
transition: bottom 1s ease-in-out;
&.showIt {
bottom: -2px;
}
2024-09-08 23:49:43 +08:00
}
2024-09-27 21:21:23 +08:00
.bottom-tips {
position: absolute;
right: 40%;
2024-09-27 21:21:23 +08:00
bottom: -40px;
transition: bottom 1s ease-in-out;
&.showIt {
bottom: -2px;
}
}
.top-tips {
position: absolute;
right: 40%;
2024-09-27 21:21:23 +08:00
top: -40px;
transition: top 1s ease-in-out;
&.showIt {
top: -2px;
}
}
2024-09-05 10:49:44 +08:00
}
.input-box {
width: 100%;
display: flex;
position: relative;
.input-box-header {
width: 100%;
2024-09-05 11:24:58 +08:00
height: auto;
2024-09-05 10:49:44 +08:00
padding: 0;
2024-09-05 11:24:58 +08:00
display: flex;
flex-direction: column;
2024-09-05 10:49:44 +08:00
.tool-set {
display: flex;
justify-content: space-between;
.left-tools {
display: flex;
}
.right-tools {
margin-right: 10px;
}
}
}
.input-box-main {
width: 100%;
padding: 0;
}
}
}
}
}
}
</style>