历史消息功能

This commit is contained in:
bob
2025-05-20 21:16:32 +08:00
parent ddbf6614e7
commit a011501089
7 changed files with 671 additions and 115 deletions

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
]
}

View File

@@ -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,

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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; /* 兼容性更好的换行 */
}
}
}