mirror of
https://gitee.com/lijingbo-2021/open-anylink-web.git
synced 2025-12-30 02:52:26 +00:00
历史消息功能
This commit is contained in:
@@ -12,6 +12,10 @@ export const msgChatPullMsgService = (obj) => {
|
||||
return request.get('/chat/pullMsg', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatHistoryService = (obj) => {
|
||||
return request.get('/chat/history', { params: obj })
|
||||
}
|
||||
|
||||
export const msgChatRevokeMsgService = (obj) => {
|
||||
return request.post('/chat/revokeMsg', obj)
|
||||
}
|
||||
|
||||
@@ -270,6 +270,18 @@ export const smartMatch = (content, key) => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础匹配:忽略大小写
|
||||
* @param {*} content 匹配内容
|
||||
* @param {*} key 关键字
|
||||
* @returns
|
||||
*/
|
||||
export const baseMatch = (content, key) => {
|
||||
const lowerKey = key.toLowerCase()
|
||||
const lowerContent = content.toLowerCase()
|
||||
return lowerContent.includes(lowerKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 汉字转全拼(小写,无空格)
|
||||
* @param {*} name
|
||||
|
||||
@@ -160,29 +160,58 @@ export const imageTypes = () => {
|
||||
return [
|
||||
msgContentType.IMAGE,
|
||||
msgContentType.SCREENSHOT,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT,
|
||||
msgContentType.SCREENSHOT || msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT || msgContentType.AT,
|
||||
msgContentType.SCREENSHOT || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT,
|
||||
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT || msgContentType.AT || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.EMOJI || msgContentType.AT,
|
||||
msgContentType.SCREENSHOT || msgContentType.TEXT || msgContentType.AT || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT || msgContentType.EMOJI || msgContentType.AT || msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.EMOJI | msgContentType.AT | msgContentType.QUOTE,
|
||||
msgContentType.SCREENSHOT | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.QUOTE,
|
||||
|
||||
msgContentType.SCREENSHOT ||
|
||||
msgContentType.TEXT ||
|
||||
msgContentType.EMOJI ||
|
||||
msgContentType.QUOTE,
|
||||
|
||||
msgContentType.SCREENSHOT ||
|
||||
msgContentType.TEXT ||
|
||||
msgContentType.EMOJI ||
|
||||
msgContentType.AT ||
|
||||
msgContentType.SCREENSHOT |
|
||||
msgContentType.TEXT |
|
||||
msgContentType.EMOJI |
|
||||
msgContentType.AT |
|
||||
msgContentType.QUOTE
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有包含Quote的type集合
|
||||
* @returns
|
||||
*/
|
||||
export const quoteTypes = () => {
|
||||
return [
|
||||
msgContentType.QUOTE,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.TEXT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI,
|
||||
msgContentType.QUOTE | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.SCREENSHOT,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.AT,
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
|
||||
msgContentType.QUOTE | msgContentType.EMOJI | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.AT | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.SCREENSHOT,
|
||||
msgContentType.QUOTE | msgContentType.TEXT | msgContentType.EMOJI | msgContentType.AT,
|
||||
|
||||
msgContentType.QUOTE |
|
||||
msgContentType.TEXT |
|
||||
msgContentType.EMOJI |
|
||||
msgContentType.AT |
|
||||
msgContentType.SCREENSHOT
|
||||
]
|
||||
}
|
||||
|
||||
@@ -579,7 +579,16 @@ const sendRead = () => {
|
||||
selectedSession.value.sessionType === MsgType.CHAT
|
||||
? MsgType.CHAT_READ
|
||||
: MsgType.GROUP_CHAT_READ
|
||||
wsConnect.sendMsg(selectedSessionId.value, showId.value, msgType, content + '', 0, '', () => {})
|
||||
wsConnect.sendMsg(
|
||||
selectedSessionId.value,
|
||||
showId.value,
|
||||
msgType,
|
||||
content + '',
|
||||
0,
|
||||
null,
|
||||
() => {},
|
||||
() => {}
|
||||
)
|
||||
// 更新本地缓存的已读位置
|
||||
messageData.updateSession({
|
||||
sessionId: selectedSessionId.value,
|
||||
|
||||
@@ -1,55 +1,104 @@
|
||||
<script setup lang="jsx">
|
||||
import { ref, onMounted, computed, watch, createApp, h } from 'vue'
|
||||
import { ElDialog, ElLoading, ElIcon } from 'element-plus'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { ref, computed, watch, createApp, h, nextTick } from 'vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { Close, Filter, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
useUserStore,
|
||||
useUserCardStore,
|
||||
useGroupStore,
|
||||
useMessageStore,
|
||||
useImageStore,
|
||||
useAudioStore,
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import { showTimeFormat, jsonParseSafe } from '@/js/utils/common'
|
||||
import { showTimeFormat, jsonParseSafe, baseMatch } from '@/js/utils/common'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import router from '@/router'
|
||||
import { msgContentType } from '@/const/msgConst'
|
||||
import { BEGIN_MSG_ID, msgContentType } from '@/const/msgConst'
|
||||
import MsgBoxRecording from '@/views/message/components/MsgBoxRecording.vue'
|
||||
import MsgBoxImage from '@/views/message/components/MsgBoxImage.vue'
|
||||
import MsgBoxAudio from '@/views/message/components/MsgBoxAudio.vue'
|
||||
import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
|
||||
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
||||
import DialogForMsgForward from '@/views/message/components/DialogForMsgForward.vue'
|
||||
import HashNoData from '@/components/common/HasNoData.vue'
|
||||
import { emojis } from '@/js/utils/emojis'
|
||||
import { msgChatQueryMessagesService } from '@/api/message'
|
||||
import { showSimplifyMsgContent } from '@/js/utils/message'
|
||||
import { msgChatHistoryService, msgChatQueryMessagesService } from '@/api/message'
|
||||
import { imageTypes, quoteTypes, showSimplifyMsgContent } from '@/js/utils/message'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const props = defineProps(['isShow', 'title', 'sessionId', 'msgs', 'tier'])
|
||||
const props = defineProps(['isShow', 'sessionId'])
|
||||
const emit = defineEmits(['update:isShow', 'showUserCard', 'close'])
|
||||
|
||||
const userData = useUserStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const groupData = useGroupStore()
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
|
||||
const tabOption = ref('all')
|
||||
const forwardMsgs = ref({})
|
||||
const quoteMsg = ref({})
|
||||
const isFilter = ref(false)
|
||||
const keyword = ref('')
|
||||
const timeRange = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
try {
|
||||
await messageData.preloadResource(props.msgs)
|
||||
await loadRelatedMsg()
|
||||
} finally {
|
||||
loadingInstance.close()
|
||||
const elTabOptions = [
|
||||
{ label: '全部', name: 'all' },
|
||||
{ label: '图片', name: 'image' },
|
||||
{ label: '语音', name: 'recording' },
|
||||
{ label: '音频', name: 'audio' },
|
||||
{ label: '视频', name: 'video' },
|
||||
{ label: '文件', name: 'document' },
|
||||
{ label: '@我', name: 'at' },
|
||||
{ label: '引用', name: 'quote' },
|
||||
{ label: '聊天记录', name: 'forward' }
|
||||
]
|
||||
|
||||
const timeRangeShortcuts = [
|
||||
{
|
||||
text: '最近一周',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 7)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近一月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setMonth(start.getMonth() - 1)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近三月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setMonth(start.getMonth() - 3)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近半年',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setMonth(start.getMonth() - 6)
|
||||
return [start, end]
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
/**
|
||||
* 切换session时要强制关闭:比如点击列表中头像 => 弹出的UserCard => 点击发送消息按钮
|
||||
@@ -61,8 +110,214 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const initConfig = () => {
|
||||
isFilter.value = false
|
||||
endIndex.value = step
|
||||
keyword.value = ''
|
||||
timeRange.value = []
|
||||
pullDoneFlag.value = false
|
||||
isAtbottom.value = false
|
||||
|
||||
nextTick(() => {
|
||||
const element = document.querySelector(`#dialog-msg-item-container-${tabOption.value}`)
|
||||
element.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'instant'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
historyMsgsAll.value = []
|
||||
historyMsgsImage.value = []
|
||||
historyMsgsRecording.value = []
|
||||
historyMsgsAudio.value = []
|
||||
historyMsgsVideo.value = []
|
||||
historyMsgsDocument.value = []
|
||||
historyMsgsAt.value = []
|
||||
historyMsgsQuote.value = []
|
||||
historyMsgsForward.value = []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isShow,
|
||||
async (newValue) => {
|
||||
// 打开历史消息界面
|
||||
if (newValue) {
|
||||
tabOption.value = 'all'
|
||||
initConfig()
|
||||
initData()
|
||||
|
||||
const msgs = messageData.msgRecordsList[props.sessionId]
|
||||
if (msgs) {
|
||||
historyMsgsAll.value = Object.values(msgs).sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeB - timeA
|
||||
})
|
||||
nextTick(() => {
|
||||
loadRelatedMsg()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const tabContentTypes = computed(() => {
|
||||
switch (tabOption.value) {
|
||||
case 'all':
|
||||
return [0]
|
||||
case 'image':
|
||||
return imageTypes()
|
||||
case 'recording':
|
||||
return [msgContentType.RECORDING]
|
||||
case 'audio':
|
||||
return [msgContentType.AUDIO]
|
||||
case 'video':
|
||||
return [msgContentType.VIDEO]
|
||||
case 'document':
|
||||
return [msgContentType.DOCUMENT]
|
||||
case 'at':
|
||||
return [msgContentType.AT]
|
||||
case 'quote':
|
||||
return quoteTypes()
|
||||
case 'forward':
|
||||
return [msgContentType.FORWARD]
|
||||
default:
|
||||
return [0]
|
||||
}
|
||||
})
|
||||
|
||||
const historyMsgsAll = ref([])
|
||||
const historyMsgsImage = ref([])
|
||||
const historyMsgsRecording = ref([])
|
||||
const historyMsgsAudio = ref([])
|
||||
const historyMsgsVideo = ref([])
|
||||
const historyMsgsDocument = ref([])
|
||||
const historyMsgsAt = ref([])
|
||||
const historyMsgsQuote = ref([])
|
||||
const historyMsgsForward = ref([])
|
||||
|
||||
const historyMsgs = computed(() => {
|
||||
switch (tabOption.value) {
|
||||
case 'all':
|
||||
return historyMsgsAll.value
|
||||
case 'image':
|
||||
return historyMsgsImage.value
|
||||
case 'recording':
|
||||
return historyMsgsRecording.value
|
||||
case 'audio':
|
||||
return historyMsgsAudio.value
|
||||
case 'video':
|
||||
return historyMsgsVideo.value
|
||||
case 'document':
|
||||
return historyMsgsDocument.value
|
||||
case 'at':
|
||||
return historyMsgsAt.value
|
||||
case 'quote':
|
||||
return historyMsgsQuote.value
|
||||
case 'forward':
|
||||
return historyMsgsForward.value
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const historyMsgsShow = computed(() => {
|
||||
// 过滤关键字
|
||||
let data
|
||||
if (!keyword.value) {
|
||||
data = historyMsgs.value
|
||||
} else {
|
||||
data = historyMsgs.value.filter((msg) => {
|
||||
const arr = jsonParseSafe(msg.content)
|
||||
// 不允许非结构化的content
|
||||
if (!arr) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const item of arr) {
|
||||
if (!item.type || !item.value) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case msgContentType.TEXT:
|
||||
if (baseMatch(item.value, keyword.value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case msgContentType.EMOJI:
|
||||
continue
|
||||
case msgContentType.SCREENSHOT:
|
||||
continue
|
||||
case msgContentType.AT:
|
||||
continue
|
||||
case msgContentType.QUOTE:
|
||||
continue
|
||||
|
||||
case msgContentType.IMAGE:
|
||||
if (baseMatch(imageData.image[item.value].fileName, keyword.value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case msgContentType.RECORDING:
|
||||
continue
|
||||
case msgContentType.AUDIO:
|
||||
if (baseMatch(audioData.audio[item.value].fileName, keyword.value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case msgContentType.VIDEO:
|
||||
if (baseMatch(videoData.video[item.value].fileName, keyword.value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case msgContentType.DOCUMENT:
|
||||
if (baseMatch(documentData.document[item.value].fileName, keyword.value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case msgContentType.FORWARD:
|
||||
continue
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return data.slice(0, endIndex.value)
|
||||
})
|
||||
|
||||
const historyMsgsAddData = async (list) => {
|
||||
await messageData.preloadResource(list)
|
||||
|
||||
for (const item of list) {
|
||||
historyMsgs.value.push(item)
|
||||
}
|
||||
|
||||
historyMsgs.value.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeB - timeA
|
||||
})
|
||||
|
||||
await loadRelatedMsg()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载引用消息和聊天记录中的消息
|
||||
*/
|
||||
const loadRelatedMsg = async () => {
|
||||
for (const msg of props.msgs) {
|
||||
for (const msg of historyMsgs.value) {
|
||||
const content = msg.content
|
||||
const arr = jsonParseSafe(content)
|
||||
if (!arr) {
|
||||
@@ -71,6 +326,10 @@ const loadRelatedMsg = async () => {
|
||||
|
||||
for (const item of arr) {
|
||||
if (item.type === msgContentType.QUOTE) {
|
||||
if (quoteMsg.value[msg.msgId]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 先从本地消息缓存中获取
|
||||
const msgFromStore = messageData.getMsg(msg.sessionId, item.value.msgId)
|
||||
if (!msgFromStore.msgId) {
|
||||
@@ -87,7 +346,9 @@ const loadRelatedMsg = async () => {
|
||||
quoteMsg.value[msg.msgId] = msgFromStore
|
||||
}
|
||||
} else if (item.type === msgContentType.FORWARD) {
|
||||
if (!forwardMsgs.value[msg.msgId]) {
|
||||
if (forwardMsgs.value[msg.msgId] && forwardMsgs.value[msg.msgId].length > 0) {
|
||||
continue
|
||||
} else {
|
||||
forwardMsgs.value[msg.msgId] = []
|
||||
}
|
||||
|
||||
@@ -118,14 +379,26 @@ const loadRelatedMsg = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const usersInfo = computed(() => {
|
||||
const session = messageData.sessionList[props.sessionId]
|
||||
if (session.sessionType === MsgType.CHAT) {
|
||||
return {
|
||||
[myAccount.value]: userData.user,
|
||||
[session.objectInfo.account]: session.objectInfo
|
||||
}
|
||||
} else if (session.sessionType === MsgType.GROUP_CHAT) {
|
||||
const groupId = session.remoteId
|
||||
const members = groupData.groupMembersList[groupId]
|
||||
return members
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
const myAccount = computed(() => {
|
||||
return userData.user.account
|
||||
})
|
||||
|
||||
const isMyAccount = (account) => {
|
||||
return myAccount.value === account
|
||||
}
|
||||
|
||||
const renderContent = ({ msg }) => {
|
||||
const content = msg.content
|
||||
const msgId = msg.msgId
|
||||
@@ -163,7 +436,7 @@ const renderContent = ({ msg }) => {
|
||||
case msgContentType.DOCUMENT:
|
||||
return renderDocument(item.value)
|
||||
case msgContentType.FORWARD:
|
||||
return renderForwardTogether(item.value, msgId)
|
||||
return renderForward(item.value, msgId)
|
||||
default:
|
||||
return <span></span>
|
||||
}
|
||||
@@ -260,7 +533,7 @@ const renderDocument = (documentId) => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderForwardTogether = (forwardContent, msgId) => {
|
||||
const renderForward = (forwardContent, msgId) => {
|
||||
const msgs = forwardMsgs.value[msgId]
|
||||
if (!msgs) {
|
||||
return <div class={'forward-together'}></div>
|
||||
@@ -306,7 +579,7 @@ const renderForwardTogether = (forwardContent, msgId) => {
|
||||
title,
|
||||
sessionId: msgsSorted[0].sessionId,
|
||||
msgs: msgsSorted,
|
||||
tier: (props.tier || 0) + 1,
|
||||
tier: 0,
|
||||
onClose: () => {
|
||||
app.unmount()
|
||||
document.body.removeChild(container)
|
||||
@@ -388,84 +661,235 @@ const onShowUserCard = (account) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pullDoneFlag = ref(false) // 消息拉取是否结束
|
||||
const isAtbottom = ref(false) // 消息是否到达底部
|
||||
const isLoadingMsg = ref(false) // 是否正在加载消息
|
||||
const step = 10
|
||||
const endIndex = ref(step)
|
||||
|
||||
const loadTipsStr = computed(() => {
|
||||
if (pullDoneFlag.value) {
|
||||
return '没有更多消息了'
|
||||
} else if (isLoadingMsg.value) {
|
||||
return '加载中...'
|
||||
} else {
|
||||
return '加载更多'
|
||||
}
|
||||
})
|
||||
|
||||
const pullMsg = async () => {
|
||||
if (pullDoneFlag.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const endMsgId =
|
||||
historyMsgs.value.length > 0 ? historyMsgs.value[historyMsgs.value.length - 1].msgId : 0
|
||||
if (endMsgId === BEGIN_MSG_ID) {
|
||||
return
|
||||
}
|
||||
|
||||
const pageSize = 30
|
||||
const params = {
|
||||
sessionId: props.sessionId,
|
||||
pageSize,
|
||||
...(tabOption.value !== 'all' && { contentTypes: tabContentTypes.value.join(',') }),
|
||||
...(endMsgId && { endMsgId }),
|
||||
...(timeRange.value &&
|
||||
timeRange.value.length > 1 && {
|
||||
startTime: timeRange.value[0].getTime(),
|
||||
endTime: timeRange.value[1].getTime()
|
||||
})
|
||||
}
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
isLoadingMsg.value = true
|
||||
msgChatHistoryService(params)
|
||||
.then(async (res) => {
|
||||
const list = res.data.data.msgList
|
||||
const totalCount = res.data.data.count
|
||||
if (totalCount > 0) {
|
||||
await historyMsgsAddData(list)
|
||||
isAtbottom.value = false
|
||||
}
|
||||
|
||||
// 如果totalCount比pageSize少,说明服务器没有更多数据了
|
||||
if (totalCount < pageSize) {
|
||||
pullDoneFlag.value = true
|
||||
}
|
||||
|
||||
if (list.length > step) {
|
||||
endIndex.value += step
|
||||
} else {
|
||||
endIndex.value += list.length
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isLoadingMsg.value = false
|
||||
loadingInstance.close()
|
||||
})
|
||||
}
|
||||
|
||||
let noMoreMsgTipsTimer = null
|
||||
const handleListWheel = (tab) => {
|
||||
const element = document.querySelector(`#dialog-msg-item-container-${tab}`)
|
||||
const clientHeight = element.clientHeight // 容器高度
|
||||
const scrollHeight = element.scrollHeight // 滚动条高度
|
||||
const scrollTop = element.scrollTop // 当前滚动位置
|
||||
const isScrollAtBottom = scrollTop + clientHeight >= scrollHeight - 10 // 判断是否滚动到底部, 10个像素点误差
|
||||
console.log(
|
||||
`clientHeight: ${clientHeight}, scrollHeight: ${scrollHeight}, scrollTop: ${scrollTop}`
|
||||
)
|
||||
|
||||
if (isScrollAtBottom) {
|
||||
isAtbottom.value = true
|
||||
const diff = historyMsgs.value.length - endIndex.value
|
||||
if (diff >= step) {
|
||||
endIndex.value += step
|
||||
} else if (diff < step && diff > 0) {
|
||||
endIndex.value = historyMsgs.value.length
|
||||
} else if (diff <= 0 && !isLoadingMsg.value && !pullDoneFlag.value) {
|
||||
pullMsg()
|
||||
} else if (pullDoneFlag.value) {
|
||||
clearTimeout(noMoreMsgTipsTimer)
|
||||
noMoreMsgTipsTimer = setTimeout(() => {
|
||||
ElMessage.warning('没有更多消息了')
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
isAtbottom.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onTabChange = async () => {
|
||||
initConfig()
|
||||
|
||||
if (historyMsgs.value.length === 0) {
|
||||
await pullMsg()
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmTimeFilter = async () => {
|
||||
initData()
|
||||
endIndex.value = step
|
||||
pullDoneFlag.value = false
|
||||
isAtbottom.value = false
|
||||
await pullMsg()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dialog-msg-list-wrapper">
|
||||
<div class="dialog-msg-history-wrapper">
|
||||
<el-dialog
|
||||
class="dialog-msg-list"
|
||||
class="dialog-msg-history"
|
||||
:model-value="props.isShow"
|
||||
:modal="false"
|
||||
draggable
|
||||
:width="'600px'"
|
||||
:top="`${30 + (props.tier || 0)}vh`"
|
||||
:width="'800px'"
|
||||
:top="'20vh'"
|
||||
:z-index="1000"
|
||||
:style="{
|
||||
minHeight: '360px',
|
||||
marginLeft: `calc(50% - 300px + ${props.tier || 0} * 1vw)`
|
||||
minHeight: '720px'
|
||||
}"
|
||||
:show-close="false"
|
||||
@closed="onClose"
|
||||
>
|
||||
<template #header>
|
||||
<span class="title bdr-b">{{ props.title }}</span>
|
||||
<span class="title bdr-b">历史消息</span>
|
||||
<el-icon class="close-button" @click="onClose"><Close /></el-icon>
|
||||
</template>
|
||||
<div class="dialog-msg-item-container my-scrollbar">
|
||||
<div
|
||||
v-for="item in props.msgs"
|
||||
:key="item.msgId"
|
||||
class="dialog-msg-item"
|
||||
:style="{
|
||||
flexDirection: isMyAccount(item.fromId) ? 'row-reverse' : 'row',
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
|
||||
<el-tabs v-model="tabOption" type="card" @tab-change="onTabChange">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in elTabOptions"
|
||||
:key="index"
|
||||
:label="item.label"
|
||||
:name="item.name"
|
||||
>
|
||||
<div class="dialog-msg-item-avatar">
|
||||
<UserAvatarIcon
|
||||
class="avatar-message-item"
|
||||
:size="'small'"
|
||||
:showId="item.fromId"
|
||||
:showName="item.nickName"
|
||||
@click="onShowUserCard(item.fromId)"
|
||||
></UserAvatarIcon>
|
||||
</div>
|
||||
<div class="dialog-msg-item-main">
|
||||
<div
|
||||
class="dialog-msg-item-header"
|
||||
:style="{
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
>
|
||||
<div class="dialog-msg-item-nickname">{{ item.nickName }}</div>
|
||||
<div class="dialog-msg-item-time">{{ showTimeFormat(item.msgTime) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="dialog-msg-item-body"
|
||||
:style="{
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="dialog-msg-item-content"
|
||||
:style="{
|
||||
borderTopLeftRadius: isMyAccount(item.fromId) ? '10px' : '0',
|
||||
borderTopRightRadius: isMyAccount(item.fromId) ? '0' : '10px',
|
||||
backgroundColor: isMyAccount(item.fromId) ? '#c6e2ff' : '#dedfe0'
|
||||
}"
|
||||
>
|
||||
<renderContent :msg="item" />
|
||||
<div
|
||||
class="dialog-msg-item-container"
|
||||
:class="{ 'my-scrollbar': historyMsgsShow.length > 0 }"
|
||||
:id="`dialog-msg-item-container-${item.name}`"
|
||||
@wheel="handleListWheel(item.name)"
|
||||
>
|
||||
<div v-for="item in historyMsgsShow" :key="item.msgId" class="dialog-msg-item">
|
||||
<div class="dialog-msg-item-avatar">
|
||||
<UserAvatarIcon
|
||||
class="avatar-message-item"
|
||||
:size="'small'"
|
||||
:showId="item.fromId"
|
||||
:showName="usersInfo[item.fromId].nickName"
|
||||
:showAvatarThumb="usersInfo[item.fromId].avatarThumb"
|
||||
@click="onShowUserCard(item.fromId)"
|
||||
></UserAvatarIcon>
|
||||
</div>
|
||||
<div class="dialog-msg-item-main">
|
||||
<div class="dialog-msg-item-header">
|
||||
<div class="dialog-msg-item-nickname">{{ usersInfo[item.fromId].nickName }}</div>
|
||||
<div class="dialog-msg-item-time">{{ showTimeFormat(item.msgTime) }}</div>
|
||||
</div>
|
||||
<div class="dialog-msg-item-body">
|
||||
<div class="dialog-msg-item-content">
|
||||
<renderContent :msg="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HashNoData v-if="historyMsgsShow.length === 0" :size="100"></HashNoData>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-button
|
||||
class="filter-cion"
|
||||
:type="isFilter ? 'primary' : 'default'"
|
||||
:icon="Filter"
|
||||
title="过滤"
|
||||
circle
|
||||
@click="isFilter = !isFilter"
|
||||
/>
|
||||
|
||||
<div v-if="isFilter" class="filters">
|
||||
<div class="filter-keyword filter-item">
|
||||
<el-input
|
||||
v-model.trim="keyword"
|
||||
placeholder="搜索:关键字"
|
||||
:prefix-icon="Search"
|
||||
:clearable="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-time filter-item">
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
:shortcuts="timeRangeShortcuts"
|
||||
range-separator="到"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
@change="handleConfirmTimeFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-icon class="close-button" @click="isFilter = false"><Close /></el-icon>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isAtbottom"
|
||||
class="load-tips"
|
||||
:style="{
|
||||
cursor: pullDoneFlag ? 'default' : 'pointer',
|
||||
color: pullDoneFlag ? 'gray' : '#409eff'
|
||||
}"
|
||||
@click="pullMsg()"
|
||||
>
|
||||
{{ loadTipsStr }}
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-msg-list-wrapper {
|
||||
.dialog-msg-history-wrapper {
|
||||
:deep(.el-dialog) {
|
||||
.el-dialog__header {
|
||||
position: relative;
|
||||
@@ -496,14 +920,19 @@ const onShowUserCard = (account) => {
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-msg-list {
|
||||
.dialog-msg-history {
|
||||
position: relative;
|
||||
|
||||
.my-scrollbar {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.dialog-msg-item-container {
|
||||
max-height: 480px;
|
||||
height: 560px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 0 5px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.dialog-msg-item {
|
||||
display: flex;
|
||||
@@ -526,11 +955,74 @@ const onShowUserCard = (account) => {
|
||||
.dialog-msg-item-content {
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 10px;
|
||||
background-color: #dedfe0;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-cion {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 80px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: absolute;
|
||||
left: calc(50% - 234px);
|
||||
top: 150px;
|
||||
padding: 24px 24px 16px 24px;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
background-color: #f5f5f5;
|
||||
.filter-time {
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-keyword {
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: gray;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-tips {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: calc(50% - 60px);
|
||||
bottom: 5px;
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ElMessage } from 'element-plus'
|
||||
import EmojiIcon from '@/assets/svg/emoji.svg'
|
||||
import FileIcon from '@/assets/svg/file.svg'
|
||||
import ImageIcon from '@/assets/svg/image.svg'
|
||||
import CodeIcon from '@/assets/svg/code.svg'
|
||||
import VoteIcon from '@/assets/svg/vote.svg'
|
||||
// import CodeIcon from '@/assets/svg/code.svg'
|
||||
// import VoteIcon from '@/assets/svg/vote.svg'
|
||||
import EmojiBox from '@/views/message/components/EmojiBox.vue'
|
||||
import InputTool from '@/views/message/components/InputTool.vue'
|
||||
import { mtsUploadService, mtsUploadServiceForImage } from '@/api/mts'
|
||||
@@ -23,6 +23,7 @@ import { prehandleImage } from '@/js/utils/image'
|
||||
import { prehandleVideo } from '@/js/utils/video'
|
||||
import { getMd5 } from '@/js/utils/file'
|
||||
import AgreeBeforeSend from '@/views/message/components/AgreeBeforeSend.vue'
|
||||
import DialogForMsgHistory from './DialogForMsgHistory.vue'
|
||||
|
||||
const props = defineProps(['sessionId', 'isShowToolSet'])
|
||||
const emit = defineEmits(['sendEmoji', 'showRecorder', 'sendMessage', 'saveLocalMsg'])
|
||||
@@ -34,6 +35,7 @@ const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
const isShowEmojiBox = ref(false)
|
||||
const showAgreeDialog = ref(false)
|
||||
const isShowHistoryDialog = ref(false)
|
||||
|
||||
const session = computed(() => {
|
||||
return messageData.sessionList[props.sessionId]
|
||||
@@ -238,6 +240,10 @@ const showRecorder = () => {
|
||||
emit('showRecorder')
|
||||
}
|
||||
|
||||
const showHistory = () => {
|
||||
isShowHistoryDialog.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
closeWindow
|
||||
})
|
||||
@@ -279,17 +285,17 @@ defineExpose({
|
||||
<Microphone />
|
||||
</template>
|
||||
</InputTool>
|
||||
<InputTool tips="代码" @click="ElMessage.warning('功能开发中')">
|
||||
<!-- <InputTool tips="代码" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<CodeIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
</InputTool> -->
|
||||
<!-- <InputTool tips="位置" @click="ElMessage.warning('功能开发中')">
|
||||
<template #iconSlot>
|
||||
<LocationInformation />
|
||||
</template>
|
||||
</InputTool> -->
|
||||
<InputTool
|
||||
<!-- <InputTool
|
||||
v-if="messageData.sessionList[props.sessionId].sessionType === MsgType.GROUP_CHAT"
|
||||
tips="群投票"
|
||||
@click="ElMessage.warning('功能开发中')"
|
||||
@@ -297,10 +303,10 @@ defineExpose({
|
||||
<template #iconSlot>
|
||||
<VoteIcon />
|
||||
</template>
|
||||
</InputTool>
|
||||
</InputTool> -->
|
||||
</div>
|
||||
<div class="right-tools">
|
||||
<InputTool tips="聊天记录" @click="ElMessage.warning('功能开发中')">
|
||||
<InputTool tips="历史消息" @click="showHistory">
|
||||
<template #iconSlot>
|
||||
<Clock />
|
||||
</template>
|
||||
@@ -321,6 +327,10 @@ defineExpose({
|
||||
:src="localSrc"
|
||||
@confirm="onConfirmSendFile"
|
||||
></AgreeBeforeSend>
|
||||
<DialogForMsgHistory
|
||||
v-model:isShow="isShowHistoryDialog"
|
||||
:sessionId="props.sessionId"
|
||||
></DialogForMsgHistory>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1314,9 +1314,9 @@ const handleItemClick = () => {
|
||||
border-radius: 10px;
|
||||
border-top-right-radius: 0;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word; /* 长单词或URL强制换行 */
|
||||
overflow-wrap: break-word; /* 兼容性更好的换行 */
|
||||
// white-space: pre-wrap;
|
||||
// word-break: break-word; /* 长单词或URL强制换行 */
|
||||
// overflow-wrap: break-word; /* 兼容性更好的换行 */
|
||||
}
|
||||
|
||||
.my-message-status {
|
||||
@@ -1401,9 +1401,9 @@ const handleItemClick = () => {
|
||||
border-radius: 10px;
|
||||
border-top-left-radius: 0;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word; /* 长单词或URL强制换行 */
|
||||
overflow-wrap: break-word; /* 兼容性更好的换行 */
|
||||
// white-space: pre-wrap;
|
||||
// word-break: break-word; /* 长单词或URL强制换行 */
|
||||
// overflow-wrap: break-word; /* 兼容性更好的换行 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user