查询历史聊天记录
多条件查询记录是经常需要进行编写的功能,这里就以多条件查询历史聊天记录为例子来介绍如何进行基于关键字、日期、聊天记录类型 三种条件进行模糊查询、日期拼接、条件拼接查询。
前端
抽屉管理
首先我们控制 element ui 的 抽屉的打开与关闭
这里可能会出现父子组件传值的错误,详情请看这篇文章
由于 element ui 的 el-date-picker
组件也存在父子组件关系,所以这个还会报错,但是不影响使用了
日期查询配置
日期查询我们就使用 element ui 的 date-picker 组件
format
:选中日期后显示在页面时的格式value-format
: 选中日期后传值时的格式picker-options
:这里我们绑定一个选中日期不可超过今天的规定change
:在我们更换日期后,检测到立刻重新请求聊天记录
<el-date-picker
v-model="searchDate"
type="date"
placeholder="选择日期"
size="mini"
format="yyyy 年 MM 月 dd 日"
value-format="yyyy-MM-dd"
@change="searchDateChange"
:picker-options="pickerOptions"/>
必须晚于今天
pickerOptions: { // 必须晚于今天
disabledDate (time) {
return time.getTime() > Date.now()
}
},
首次加载数据
首次加载时,我们首先将起始的页码设置为了 1,首次应该加载第 0 页,所以我们直接将 pageIndex 进行 -1 操作,此时的分页组件上就会显示第一页的高亮了,而且访问到的数据也是真正的第一页
pageIndex: this.pageIndex - 1,
每次切换加载的记录
当切换的时候组件没有进行销毁,所以历史记录所在的抽屉的数据不会变化,因此我们每次加载时切换会话,每个会话都有唯一的 roomId,我们将其作为 key 进行属性的绑定,此时组件就会重新生成了
虚拟DOM中key的作用
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】
随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下
对比规则:
就虚拟DOM中找到了与新虚拟DOM相同的key
若虚拟DOM中内容没变,直接使用之前的真实DOM!
若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
就虚拟DOM中未找到与新虚拟DOM相同的key创建新的真实DOM,随后渲染到页面。
<history-message
@changeHistoryDrawerVisible="changeHistoryDrawerVisible"
:historyDrawer="historyDrawer"
:roomId="currentConversation.roomId"
:key="currentConversation.roomId"
:conversationType="currentConversation.type">
</history-message>
前端全部代码
MyMain.vue
<template>
<div class="chat-area">
<div class="main">
<div class="message-list-container">
<!-- 聊天列表 -->
<message-list ref='messagelist'
@load-message="loadmessage"
:messagelist="messages"
:hasmore="hasMore"
:scrollbottom="scrollBottom"
>
</message-list>
</div>
</div>
<div class="message-edit-container">
<div class="tool">
<span class="tool-item">
<i class="item iconfont icon-emoji" @click.stop="showEmojiCom = !showEmojiCom"></i>
</span>
<span class="tool-item">
<label for="upImg">
<i class="item el-icon-picture"></i>
</label>
</span>
<span class="tool-item">
<i class="item el-icon-folder"/>
</span>
<span class="tool-item">
<i class="item iconfont icon-huaban"/>
</span>
<span class="tool-item">
<i class="item iconfont icon-shipin"/>
</span>
<span class="tool-item">
<i class="item el-icon-phone-outline"/>
</span>
<span class="tool-item" >
<i class="item el-icon-caret-bottom" @click="changeHistoryDrawerVisible(true)">历史记录</i>
</span>
</div>
<div class="operation">
<el-button @click="send" type="success" size="small" plain>发送</el-button>
</div>
<textarea ref="chatInp" class="textarea" v-model="messageText" maxlength="200" @input="scrollBottom = true"
@keydown.enter="send($event)"></textarea>
<custom-emoji v-if="showEmojiCom" class="emoji-component" @addemoji="addEmoji"/>
<history-message
@changeHistoryDrawerVisible="changeHistoryDrawerVisible"
:historyDrawer="historyDrawer"
:roomId="currentConversation.roomId"
:key="currentConversation.roomId"
:conversationType="currentConversation.type"></history-message>
</div>
</div>
</template>
<script>
import './../../../static/iconfont/iconfont.css'
import customEmoji from '@/components/customEmoji'
import historyMessage from '@/views/chat/HistoryMessage'
import messageList from '../chat/MessageList'
import {conversationTypes} from '@/const'
import singleMessageApi from '@/api/modules/friend'
import groupMessageApi from '@/api/modules/group'
import {fromatTime} from '@/utils'
export default {
name: 'MyMain',
components: {
customEmoji,
historyMessage,
messageList
},
props: ['currentConversation'],
data () {
return {
messageText: '', // 当前编辑消息内容
messages: [], // 消息列表
showEmojiCom: false, // 表情栏显示
historyDrawer: false, // 历史记录显示
pageIndex: 1, // 消息页数
pageSize: 10, // 一次加载消息量
hasMore: true, // 更多消息
scrollBottom: true // 滚到底部
}
},
computed: {
userInfo () {
return this.$store.state.user.userInfo
}
},
watch: {
currentConversation (newVal, oldVal) {
if (newVal && newVal.id) {
this.chatInpAutoFocus()
this.pageIndex = 1
this.messageText = ''
this.scrollBottom = true
this.messages = []
this.hasMore = true
this.getRecentMessages()
}
},
deep: true,
immediate: true
},
created () {
this.getRecentMessages()
document.addEventListener('click', this.handlerShowEmoji)
},
beforeDestroy () {
// console.log('chatArea BeforeDestroy')
document.removeEventListener('click', this.handlerShowEmoji)
},
sockets: {
receiveMessage (news) {
// 收到消息
if (news.roomId === this.currentConversation.roomId) {
// 是自己正所处的房间就添加消息并更新会话列表
this.messages = [...this.messages, news]
// 进来设置该房间未读消息数为0
setTimeout(() => {
this.$store.dispatch('conversation/SET_UNREAD_NUM', {type: 'clear', data: news})
}, 0)
}
}
},
methods: {
// 控制历史记录抽屉
changeHistoryDrawerVisible (val) {
this.historyDrawer = val
},
// 控制表情面板
handlerShowEmoji () {
this.showEmojiCom = false
// this.showUpFileCom = false
},
// 加载更多消息
loadmessage () {
if (this.hasMore) {
this.scrollBottom = false
if (this.hasMore) {
this.getRecentMessages()
}
}
},
// 添加表情
addEmoji (emoji = '') {
this.messageText += emoji
},
// 发送消息
send (e) {
e.preventDefault()
if (!this.messageText) {
return
}
const common = this.generatorMessageCommon()
const newMessage = {
...common,
message: this.messageText,
messageType: 0
}
let selfMessage = {
...newMessage,
createTime: fromatTime(new Date())
}
this.messages = [...this.messages, selfMessage]
this.$socket.emit('sendNewMessage', newMessage)
this.messageText = ''
this.showEmojiCom = false
// 发完消息更新自己会话列表的最新消息
if (selfMessage.conversationType === conversationTypes.group) selfMessage.message = selfMessage.senderNickname + ':' + selfMessage.message
this.$store.dispatch('conversation/SET_UNREAD_NUM', {type: 'clear', data: selfMessage})
},
// 生成发送消息部分字段
generatorMessageCommon () {
return {
roomId: this.currentConversation.roomId,
senderId: this.userInfo.id,
senderName: this.userInfo.username,
senderNickname: this.userInfo.nickname,
senderAvatar: this.userInfo.avatar,
conversationType: this.currentConversation.type
}
},
// 进入聊天信息页面,清除消息未读
getRecentMessages () {
const {roomId, type} = this.currentConversation
const params = {
roomId,
pageIndex: this.pageIndex,
pageSize: this.pageSize
}
// 私聊信息
if (type === conversationTypes.single) {
singleMessageApi.getRecentSingleMessage(params).then(res => {
if (res.code === 2000) {
// reverse() 会改变原数组,并且当前作用域的对象都会改变
res.data.recentMessage.reverse()
this.messages = [...res.data.recentMessage, ...this.messages]
if (res.data.recentMessage.length < this.pageSize) {
this.hasMore = false
this.pageIndex = 2
return
}
this.pageIndex++
}
})
} else if (type === conversationTypes.group) {
groupMessageApi.getRecentGroupMessage(params).then(res => {
if (res.code === 2000) {
// reverse() 会改变原数组,并且当前作用域的对象都会改变
res.data.recentMessage.reverse()
this.messages = [...res.data.recentMessage, ...this.messages]
if (res.data.recentMessage.length < this.pageSize) {
this.hasMore = false
this.pageIndex = 2
return
}
this.pageIndex++
}
})
console.log('获取群聊信息')
}
this.chatInpAutoFocus()
},
/** 聊天内容输入框自动聚焦 */
chatInpAutoFocus () {
this.$nextTick(() => {
this.$refs.chatInp.focus()
})
}
}
}
</script>
<style scoped>
.chat-area {
position: relative;
height: 100%;
}
.main {
display: flex;
position: relative;
height: calc(100% - 160px);
width: 100%;
}
.main .message-list-container {
position: relative;
height: 100%;
width: 75%;
flex: 1;
}
.message-edit-container {
box-sizing: border-box;
position: relative;
height: 150px;
border-top: 1px solid #cccccc;
}
.message-edit-container .tool {
width: 100%;
height: 28px;
line-height: 28px;
text-align: left;
background-color: rgba(233, 235, 238, 0.5);
padding: 0 10px;
box-sizing: border-box;
}
.message-edit-container .tool .tool-item {
cursor: pointer;
display: inline-block;
height: 100%;
position: relative;
}
.message-edit-container .tool .tool-item i {
padding: 0 5px;
}
.message-edit-container .tool .tool-item .emoji-container {
width: 400px;
height: 260px;
position: absolute;
bottom: 30px;
left: 0;
z-index: 10;
transition: all 0.2s;
}
.message-edit-container .tool .tool-item input {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.message-edit-container .tool .tool-item:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.message-edit-container .tool .tool-item.active {
background-color: rgba(255, 255, 255, 0.3);
}
.message-edit-container .tool .tool-item.active .emoji-container {
transform: scaleX(1);
opacity: 1;
}
.message-edit-container .tool i {
margin: 0;
}
.message-edit-container .operation {
position: absolute;
bottom: 10px;
right: 20px;
}
.message-edit-container .textarea {
overflow-x: hidden;
box-sizing: border-box;
height: calc(100% - 30px);
width: 100%;
outline: none;
border: none;
padding: 0 10px;
border: 0;
border-radius: 5px;
background-color: #e9ebee;
padding: 10px;
resize: none;
}
.message-edit-container .emoji-component {
position: absolute;
bottom: 101%;
}
</style>
HistoryMessage.vue
<template>
<div>
<el-drawer
title="历史记录"
:visible.sync="historyDrawerVisible"
v-if="historyDrawerVisible">
<div class="history-msg-cmp" v-loading="isLoading">
<div class="search">
<el-input
placeholder="请输入搜索内容"
prefix-icon="el-icon-search"
size="mini"
v-model="searchWord"
@keydown.native="searchWordChange"
/>
</div>
<div class="type">
<el-radio-group v-model="searchType" size="mini" @change="searchTypeChange">
<el-radio-button value="all" label="全部"></el-radio-button>
<el-radio-button value="img" label="图片"></el-radio-button>
<el-radio-button value="file" label="文件"></el-radio-button>
</el-radio-group>
<el-date-picker
v-model="searchDate"
type="date"
placeholder="选择日期"
size="mini"
format="yyyy 年 MM 月 dd 日"
value-format="yyyy-MM-dd"
@change="searchDateChange"
:picker-options="pickerOptions"/>
</div>
<div class="history-msg__body">
<div class="msg-list-container" v-if="historyMessageList.length">
<div class="msg-item" v-for="item in historyMessageList" :key="item.id">
<historyMessageItem :msg-item="item"/>
</div>
<div class="history-msg__footer">
<el-pagination
background
hide-on-single-page
@current-change="handleCurrentChange"
:current-page="pageIndex"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
/>
</div>
</div>
<div class="no-data" v-else>
<p class="text">没有数据~</p>
<empty width="150" heigth="150"></empty>
</div>
</div>
</div>
</el-drawer>
</div>
</template>
<script>
import {conversationTypes} from '@/const'
import groupApi from '@/api/modules/group'
import friendApi from '@/api/modules/friend'
const typeTextToValue = {
'全部': 'all',
'图片': 'img',
'文件': 'file'
}
export default {
name: 'HistoryMessage',
components: {
empty: () => import('@/SVGComponents/empty'),
historyMessageItem: () => import('@/views/chat/HistoryMessageItem')
},
props: {
historyDrawer: {
type: Boolean,
default: false
},
roomId: {
type: String
},
conversationType: {
type: Number
}
},
mounted () {
this.getHistoryMsg()
},
data () {
return {
historyMessageList: [],
searchType: '全部', // 消息类型
searchWord: '', // 搜索关键字
searchDate: '', // 搜索日期
pageIndex: 1, // 页码
pageSize: 10, // 页大小
total: 0, // 总记录
pickerOptions: { // 必须晚于今天
disabledDate (time) {
return time.getTime() > Date.now()
}
},
isLoading: false
}
},
computed: {
historyDrawerVisible: {
get () {
return this.historyDrawer
},
set (val) {
this.$emit('changeHistoryDrawerVisible', val)
}
}
},
methods: {
// 关闭重置
handleReset () {
this.historyMessageList = []
this.searchType = '全部'
this.searchWord = ''
this.searchDate = ''
this.pageIndex = 1
this.pageSize = 10
this.total = 0
this.isLoading = false
},
// 切换页码
handleCurrentChange (currentPage) {
this.pageIndex = currentPage
this.getHistoryMsg()
},
// 更换搜索关键字
searchWordChange () {
this.pageIndex = 1
this.getHistoryMsg()
},
// 更换搜索消息记录类型
searchTypeChange () {
this.pageIndex = 1
this.getHistoryMsg()
},
// 更换搜索日期
searchDateChange () {
this.pageIndex = 1
this.getHistoryMsg()
},
// 得到历史记录
getHistoryMsg () {
if (this.isLoading) return
this.isLoading = true
const params = {
roomId: this.roomId,
type: typeTextToValue[this.searchType],
keyword: this.searchWord,
date: this.searchDate,
pageIndex: this.pageIndex - 1,
pageSize: this.pageSize
}
console.log('历史记录params:', params)
let fetch = this.conversationType === conversationTypes.group ? groupApi : friendApi
fetch.getHistoryMessages(params).then(res => {
// console.log('历史消息:', res)
this.historyMessageList = res.data.historyMessageList.records
this.total = res.data.historyMessageList.total
this.isLoading = false
})
}
}
}
</script>
<style>
.history-msg-cmp {
display: flex;
flex-direction: column;
padding: 10px 10px 5px;
align-items: center;
height: 100%;
}
.history-msg-cmp .search {
width: 89%;
margin-bottom: 10px;
}
.history-msg__body {
margin: 0 25px;
flex: 1;
overflow-x: hidden;
}
.msg-list-container {
margin-top: 10px;
}
.msg-list-container .msg-item {
border-top: 1px solid #ededed;
}
.history-msg__footer {
text-align: center;
}
.no-data {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
}
.no-data .text {
color: #909399;
font-size: 20px;
text-align: center;
}
</style>
HistoryMessageItem
<template>
<div class="historyMsgItem">
<div class="historyMsgItem-avatar">
<el-avatar shape="square" :size="40" :src="msgItem.senderAvatar"></el-avatar>
</div>
<div class="historyMsgItem-content">
<div class="historyMsgItem-header">
<span class="secondary-font">{{ msgItem.senderNickname }}</span>
<span class="secondary-font">{{ msgItem.createTime | fromatTime }}</span>
</div>
<div class="historyMsgItem-text">
<span>{{ msgItem.message }}</span>
</div>
</div>
</div>
</template>
<script>
import {fromatTime} from '@/utils/index'
export default {
name: 'HistoryMessageItem',
props: [ 'msgItem' ],
filters: {
fromatTime (val) {
return fromatTime(val)
}
}
}
</script>
<style>
.historyMsgItem {
display: flex;
flex-direction: row;
justify-content: flex-start;
padding: 10px 0;
}
.historyMsgItem-avatar {
margin-right: 10px;
}
.historyMsgItem-content {
display: flex;
flex-direction: column;
}
.historyMsgItem-header {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 320px;
margin-bottom: 4px;
}
</style>
后端
分页信息
首先我们将页码与页面大小取出,这里我们将起始页码进行 + 1 操作,是因为 element ui 的分页组件默认第一页为 0,而我们的 mybatis-plus 默认的第一页为 1,所以我们需要为其进行调整
Page<GroupMessage> page = new Page<>(historyMsgRequestVo.getPageIndex() + 1, historyMsgRequestVo.getPageSize());
查询指定日期记录
由于我们使用 mybatis-plus,所以在查询时我们直接按照官方提供的函数来进行 sql 语句的拼接
apply
apply(String applySql, Object... params) apply(boolean condition, String applySql, Object... params)
- 拼接 sql
注意事项:
该方法可用于数据库函数 动态入参的
params
对应前面applySql
内部的{index}
部分.这样是不会有sql注入风险的,反之会有!
例:
apply("id = 1")
—>id = 1
例:
apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
—>date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
例:
apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08")
—>date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
if(StringUtils.isNotEmpty(historyMsgRequestVo.getDate())){
wrapper.apply("date_format(create_time,'%Y-%m-%d') = '" + historyMsgRequestVo.getDate() + "'");
}
模糊查询
最后就是我们要进行模糊查询了,我们的思路是
- 如果为全部聊天记录
- 如果关键字不为空,就对聊天文本内容
message
和聊天文件原名file_original_name
进行查询 - 注意我们使用了
.or()
来进行查询
- 如果关键字不为空,就对聊天文本内容
- 如果不为全部内容
- 判断是否为文件或者图片类型,然后我们对其原文件名进行模糊查询
// 如果为文件或者图片
if(!historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_ALL)){
wrapper.eq(historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_IMG), "message_type", 2)
.eq(historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_FILE), "message_type", 3)
.like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "file_original_name", historyMsgRequestVo.getKeyword());
}else {
wrapper.like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "message", historyMsgRequestVo.getKeyword())
.or().like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "file_original_name", historyMsgRequestVo.getKeyword());
}
后端全部代码
/**
* 获取分页历史聊天记录
* @param historyMsgRequestVo 包括模糊关键字
* @return
*/
@Override
public IPage<GroupMessage> getGroupHistoryMessages(HistoryMsgRequestVo historyMsgRequestVo) {
Page<GroupMessage> page = new Page<>(historyMsgRequestVo.getPageIndex() + 1, historyMsgRequestVo.getPageSize());
QueryWrapper<GroupMessage> wrapper = new QueryWrapper<>();
wrapper.eq("room_id", historyMsgRequestVo.getRoomId());
// 日期条件存在
if(StringUtils.isNotEmpty(historyMsgRequestVo.getDate())){
wrapper.apply("date_format(create_time,'%Y-%m-%d') = '" + historyMsgRequestVo.getDate() + "'");
}
// 如果为文件或者图片
if(!historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_ALL)){
wrapper.eq(historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_IMG), "message_type", 2)
.eq(historyMsgRequestVo.getType().equals(ConstValueEnum.MESSAGE_TYPE_FILE), "message_type", 3)
.like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "file_original_name", historyMsgRequestVo.getKeyword());
}else {
wrapper.like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "message", historyMsgRequestVo.getKeyword())
.or().like(StringUtils.isNotEmpty(historyMsgRequestVo.getKeyword()), "file_original_name", historyMsgRequestVo.getKeyword());
}
Page<GroupMessage> groupMessagePage = baseMapper.selectPage(page, wrapper);
return groupMessagePage;
}