完成搜索框中点击更多通讯录、更多群聊、点击通讯录、群聊、聊天记录对应的跳转,并处理加载更多场景;处理跳转到指定搜索记录的功能,并实现向上向下加载更多数据,并保持滚动位置

This commit is contained in:
wangyifeng 2025-05-29 18:04:16 +08:00
parent 331ca65db6
commit e1e11b7633
6 changed files with 340 additions and 56 deletions

View File

@ -5,6 +5,16 @@ export const ServeSeachQueryAll = (data = {}) => {
return post('/api/v1/elasticsearch/query-all', data)
}
// ES搜索用户数据
export const ServeQueryUser = (data) => {
return post('/api/v1/elasticsearch/query-user', data)
}
// ES搜索群组数据
export const ServeQueryGroup = (data) => {
return post('/api/v1/elasticsearch/query-group', data)
}
//ES搜索聊天记录-主页搜索什么都有、聊天记录
export const ServeQueryTalkRecord = (data = {}) => {
return post('/api/v1/elasticsearch/query-talk-record', data)

View File

@ -41,7 +41,13 @@
>
<searchItem
@click="clickSearchItem(searchResultKey, item)"
v-if="(props.listLimit && index < 3) || !props.listLimit"
v-if="(
searchResultKey === 'user_infos'
? (state.userInfosShowAll || (props.listLimit && index < 3))
: searchResultKey === 'combinedGroup'
? (state.groupInfosShowAll || (props.listLimit && index < 3))
: (props.listLimit && index < 3)
) || !props.listLimit"
:searchResultKey="searchResultKey"
:searchItem="item"
:searchText="state.searchText"
@ -56,8 +62,14 @@
</div>
<div
class="result-has-more"
v-if="getHasMoreResult(searchResultKey)"
@click="toMoreResultPage(searchResultKey)"
v-if="
getHasMoreResult(searchResultKey) &&
!(
(searchResultKey === 'user_infos' && state.userInfosExpand) ||
(searchResultKey === 'combinedGroup' && state.groupInfosExpand)
)
"
@click="onMoreResultClick(searchResultKey)"
>
<span class="text-[14px] font-regular">
{{ getHasMoreResult(searchResultKey) }}
@ -147,6 +159,7 @@
import { NInfiniteScroll } from 'naive-ui'
import searchItem from './searchItem.vue'
import { ref, reactive, defineEmits, defineProps, onMounted, watch } from 'vue'
import { ServeQueryUser, ServeQueryGroup } from '@/api/search'
const emits = defineEmits([
'toMoreResultPage',
@ -164,7 +177,16 @@ const state = reactive({
uid: 12303, //id
clickStayItem: '', //item
hasMore: true, //
loading: false //
loading: false, //
userInfosExpand: false, //
userInfosLoading: false, //
userInfosLastId: undefined, // last_id
userInfosShowAll: false, // "" true
groupInfosExpand: false, //
groupInfosLoading: false, //
groupInfosLastGroupId: 0, // last_group_id
groupInfosLastMemberId: 0, // last_member_id
groupInfosShowAll: false // "" true
})
const props = defineProps({
@ -214,7 +236,11 @@ const props = defineProps({
useCustomTitle: {
type: Boolean,
default: false
} //使
}, //使
selectItemInList: {
type: String,
default: ''
} //
})
onMounted(() => {
@ -247,6 +273,13 @@ watch(
emits('clickStayItemChange', state.clickStayItem)
//
emits('lastIdChange', 0, 0, 0, '', '')
state.userInfosExpand = false
state.userInfosShowAll = false
state.userInfosLastId = undefined
state.groupInfosExpand = false
state.groupInfosShowAll = false
state.groupInfosLastGroupId = 0
state.groupInfosLastMemberId = 0
queryAllSearch()
}
)
@ -384,6 +417,12 @@ const queryAllSearch = (doClearSearchResult) => {
}
}
state.pageNum = state.pageNum + 1
// userInfosLastId
if (typeof data.last_id !== 'undefined') {
state.userInfosLastId = data.last_id
} else {
state.userInfosLastId = undefined
}
} else {
if (state.pageNum === 1) {
//
@ -538,6 +577,13 @@ const clickSearchItem = (searchResultKey, searchItem) => {
//
const doLoadMore = (doClearSearchResult) => {
if (
state.userInfosLoading ||
state.userInfosShowAll ||
state.groupInfosShowAll // queryAllSearch
) {
return
}
if (!state.hasMore || state.loading) {
return
}
@ -546,6 +592,134 @@ const doLoadMore = (doClearSearchResult) => {
state.loading = false
})
}
watch(
() => props.selectItemInList,
(newVal, oldVal) => {
if (newVal) {
const selectedItem = JSON.parse(decodeURIComponent(newVal))
clickSearchItem('general_infos', selectedItem)
}
},
{
deep: true,
immediate: true
}
)
// last_id
async function loadMoreUserInfos() {
if (state.userInfosLoading) return
state.userInfosLoading = true
try {
let params = {
key: state.searchText,
last_id: state.userInfosLastId,
size: 10
}
const resp = await ServeQueryUser(params)
if (resp.code === 200 && Array.isArray(resp.data.user_infos)) {
if (!state.userInfosLastId) {
//
state.searchResult = {
...state.searchResult,
user_infos: resp.data.user_infos
}
} else {
//
state.searchResult = {
...state.searchResult,
user_infos: (state.searchResult.user_infos || []).concat(resp.data.user_infos)
}
}
state.userInfosLastId = resp.data.last_id
//
if (
!resp.data.last_id ||
(Array.isArray(resp.data.user_infos) && resp.data.user_infos.length < 10)
) {
state.userInfosExpand = true
}
}
} finally {
state.userInfosLoading = false
}
}
// "" ""
function onMoreResultClick(searchResultKey) {
if (searchResultKey === 'user_infos') {
state.userInfosShowAll = true
loadMoreUserInfos()
} else if (searchResultKey === 'combinedGroup') {
state.groupInfosShowAll = true
loadMoreGroupInfos()
} else {
emits('toMoreResultPage', searchResultKey, state.searchText)
}
}
// last_id
async function loadMoreGroupInfos() {
if (state.groupInfosLoading) return
state.groupInfosLoading = true
try {
let params = {
key: state.searchText,
last_group_id: state.groupInfosLastGroupId,
last_member_id: state.groupInfosLastMemberId,
size: 10
}
const resp = await ServeQueryGroup(params)
if (resp.code === 200) {
const groupInfos = Array.isArray(resp.data.group_infos) ? resp.data.group_infos : []
const groupMemberInfos = Array.isArray(resp.data.group_member_infos) ? resp.data.group_member_infos : []
// groupTempType
groupInfos.forEach(item => {
item.groupTempType = 'group_infos'
item.group_type = item.type //
})
groupMemberInfos.forEach(item => {
item.groupTempType = 'group_member_infos'
})
const isFirstLoad = (!state.groupInfosLastGroupId && !state.groupInfosLastMemberId) ||
(state.groupInfosLastGroupId === 0 && state.groupInfosLastMemberId === 0)
if (isFirstLoad) {
//
state.searchResult = {
...state.searchResult,
group_infos: groupInfos,
group_member_infos: groupMemberInfos,
combinedGroup: groupInfos.concat(groupMemberInfos)
}
} else {
//
const allGroupInfos = (state.searchResult.group_infos || []).concat(groupInfos)
const allGroupMemberInfos = (state.searchResult.group_member_infos || []).concat(groupMemberInfos)
state.searchResult = {
...state.searchResult,
group_infos: allGroupInfos,
group_member_infos: allGroupMemberInfos,
combinedGroup: allGroupInfos.concat(allGroupMemberInfos)
}
}
state.groupInfosLastGroupId = resp.data.last_group_id
state.groupInfosLastMemberId = resp.data.last_member_id
//
const noMoreData = (
(!groupInfos.length && !groupMemberInfos.length) ||
(resp.data.last_group_id === 0 && resp.data.last_member_id === 0)
)
if (noMoreData) {
state.groupInfosExpand = true
}
}
} finally {
state.groupInfosLoading = false
}
}
</script>
<style lang="scss" scoped>
.search-list {

View File

@ -14,8 +14,9 @@ interface Params {
interface SpecialParams extends Params {
msg_id?: string
cursor?: number
direction?: 'up' | 'down',
direction?: 'up' | 'down'
sort_sequence?: string
type?: 'loadMore'
}
interface LoadOptions {
@ -38,7 +39,11 @@ export const useTalkRecord = (uid: number) => {
talk_type: 0,
status: 0,
cursor: 0,
specialParams: undefined as SpecialParams | undefined
specialParams: undefined as SpecialParams | undefined,
isLocatingMessage: false,
isLoadingMore: false,
lastLoadMoreTime: 0,
targetMessagePosition: 0
})
// 重置 loadConfig
@ -140,10 +145,9 @@ export const useTalkRecord = (uid: number) => {
el.scrollTop = el.scrollHeight
setTimeout(() => {
console.log('el.scrollHeight',el.scrollHeight)
console.log('el.scrollHeight', el.scrollHeight)
console.log('request.cursor == 0')
el.scrollTop = el.scrollHeight + 1000
}, 500)
} else {
console.log('request.cursor !== 0')
@ -161,13 +165,13 @@ export const useTalkRecord = (uid: number) => {
const getMinSequence = () => {
console.error('records.value', records.value)
if (!records.value.length) return 0
console.error(Math.min(...records.value.map(item => item.sequence)))
return Math.min(...records.value.map(item => item.sequence))
console.error(Math.min(...records.value.map((item) => item.sequence)))
return Math.min(...records.value.map((item) => item.sequence))
}
// 获取当前消息的最大 sequence
const getMaxSequence = () => {
if (!records.value.length) return 0
return Math.max(...records.value.map(item => item.sequence))
return Math.max(...records.value.map((item) => item.sequence))
}
/**
@ -177,7 +181,10 @@ export const useTalkRecord = (uid: number) => {
*/
const onLoad = (params: Params, options?: LoadOptions) => {
// 如果会话切换,重置所有状态
if (params.talk_type !== loadConfig.talk_type || params.receiver_id !== loadConfig.receiver_id) {
if (
params.talk_type !== loadConfig.talk_type ||
params.receiver_id !== loadConfig.receiver_id
) {
resetLoadConfig()
}
@ -197,34 +204,67 @@ export const useTalkRecord = (uid: number) => {
...params,
...options.specifiedMsg
}
//msg_id是用来做定位的不做参数所以这里清空
contextParams.msg_id = ''
ServeTalkRecords(contextParams).then(({ data, code }) => {
if (code !== 200) {
loadConfig.status = 2
return
}
// dialogueStore.clearDialogueRecord()
// 记录当前滚动高度
const el = document.getElementById('imChatPanel')
const scrollHeight = el?.scrollHeight || 0
if (contextParams.direction === 'down' && !contextParams.type) {
dialogueStore.clearDialogueRecord()
}
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
dialogueStore.unshiftDialogueRecord(contextParams.direction === 'down' ? items : items.reverse())
loadConfig.status = items.length >= contextParams.limit ? 1 : 2
if (contextParams.type && contextParams.type === 'loadMore') {
dialogueStore.addDialogueRecordForLoadMore(items)
} else {
dialogueStore.unshiftDialogueRecord(
contextParams.direction === 'down' ? items : items.reverse()
)
}
if (
contextParams.direction === 'up' ||
(contextParams.direction === 'down' && !contextParams.type)
) {
loadConfig.status = items[0].sequence == 1 || data.length === 0 ? 2 : 1
}
loadConfig.cursor = data.cursor
nextTick(() => {
setTimeout(() => {
// 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
requestAnimationFrame(() => {
const el = document.getElementById('imChatPanel')
const target = document.getElementById(options.specifiedMsg?.msg_id || '')
if (el && target) {
// 如果是向上加载更多,保持原有内容位置
if (contextParams.direction === 'up') {
el.scrollTop = el.scrollHeight - scrollHeight
} else if (contextParams.type && contextParams.type === 'loadMore') {
// 如果是向下加载更多,保持目标消息在可视区域底部
// 使用可视区域高度来调整,而不是新内容的总高度
nextTick(() => {
if (el) {
el.scrollTop = scrollHeight - el.clientHeight
}
})
} else {
// 如果是定位到特定消息,计算并滚动到目标位置
const containerRect = el.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const offset = targetRect.top - containerRect.top
loadConfig.isLocatingMessage = true
// 居中
const scrollTo = el.scrollTop + offset - el.clientHeight / 2 + target.clientHeight / 2
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
addClass(target, 'border')
setTimeout(() => removeClass(target, 'border'), 3000)
}
} else if (el) {
el.scrollTop = el.scrollHeight
}
}, 50)
})
})
return
@ -243,8 +283,10 @@ export const useTalkRecord = (uid: number) => {
// 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
// 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) {
if (
loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) {
// 特殊参数模式下direction: 'up'cursor: 当前最小 sequence
onLoad(
{
@ -257,7 +299,11 @@ export const useTalkRecord = (uid: number) => {
...loadConfig.specialParams,
direction: 'up',
sort_sequence: '',
cursor: getMinSequence()
cursor: getMinSequence(),
msg_id:
records.value.find((item) =>
item.sequence === getMinSequence() ? item.msg_id : ''
)?.msg_id || ''
}
}
)
@ -281,13 +327,33 @@ export const useTalkRecord = (uid: number) => {
}
}
// 向下加载更多(兼容特殊参数模式)
// 向下加载更多(特殊参数模式才生效,普通模式无效,因为普通模式的数据就是从最新开始加载历史的,所以不需要加载更新的数据
const onLoadMoreDown = () => {
// 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
if (loadConfig.isLocatingMessage) {
loadConfig.isLocatingMessage = false
return
}
// 添加时间间隔限制,至少间隔 500ms 才能触发下一次加载
const now = Date.now()
if (now - loadConfig.lastLoadMoreTime < 500) {
return
}
loadConfig.lastLoadMoreTime = now
// 记录当前目标消息的位置
const el = document.getElementById('imChatPanel')
if (el) {
loadConfig.targetMessagePosition = el.scrollHeight - el.scrollTop
}
console.log('onLoadMoreDown 特殊模式触底了')
// 检查特殊参数是否与当前会话匹配
if (loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id) {
if (
loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) {
onLoad(
{
receiver_id: loadConfig.receiver_id,
@ -298,27 +364,23 @@ export const useTalkRecord = (uid: number) => {
specifiedMsg: {
...loadConfig.specialParams,
direction: 'down',
cursor: getMaxSequence()
sort_sequence: 'asc',
cursor: getMaxSequence(),
type: 'loadMore'
}
}
)
} else {
// 如果不匹配,重置为普通模式
resetLoadConfig()
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
}
} else {
load({
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
})
}
}
return { loadConfig, records, onLoad, onRefreshLoad, onLoadMoreDown, onJumpMessage, resetLoadConfig }
return {
loadConfig,
records,
onLoad,
onRefreshLoad,
onLoadMoreDown,
onJumpMessage,
resetLoadConfig
}
}

View File

@ -151,6 +151,10 @@ export const useDialogueStore = defineStore('dialogue', {
unshiftDialogueRecord(records) {
this.records.unshift(...records)
},
//数组尾部加入更多对话记录
addDialogueRecordForLoadMore(records){
this.records.push(...records)
},
async getGroupInfo(){
const { code, data } = await ServeGroupDetail({
group_id: this.talk.receiver_id

View File

@ -74,6 +74,24 @@ const renderChatAppSearch = () => {
console.log(searchText, searchResultKey, talk_type, receiver_id)
const result = JSON.parse(decodeURIComponent(res))
console.log(result)
if (searchResultKey === 'general_infos') {
state.ServeQueryTalkRecordParams = encodeURIComponent(
JSON.stringify({
talk_type: 0, //12
receiver_id: 0, //
last_group_id: 0, //id
last_member_id: 0, //id
last_receiver_user_name: '', //
last_receiver_group_name: '' //
})
)
state.isShowSearchRecordModal = true
state.searchRecordText = searchText
state.selectItemInList = res
} else {
talkStore.toTalk(talk_type, receiver_id, router)
}
state.showSearchDropdown = false
},
onToMoreResultPage: (searchResultKey, searchText) => {
if (searchResultKey === 'general_infos') {
@ -277,7 +295,9 @@ const state = reactive({
apiParams: '',
lastId: undefined as any,
total: 0
}
},
showSearchDropdown: false, //
selectItemInList: '' //
})
const items = computed((): ISession[] => {
@ -292,9 +312,9 @@ const items = computed((): ISession[] => {
//
const topItems = filtered
.filter(item => item.is_top === 1)
.filter((item) => item.is_top === 1)
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
const normalItems = filtered.filter(item => item.is_top !== 1)
const normalItems = filtered.filter((item) => item.is_top !== 1)
return [...topItems, ...normalItems]
})
@ -589,8 +609,8 @@ const handleGroupChatListPaginationSize = (value) => {
//
const handleClickSearchItem = (searchText, searchResultKey, talk_type, receiver_id, res) => {
console.log(searchText, searchResultKey, talk_type, receiver_id)
const result = JSON.parse(decodeURIComponent(res))
console.log(result)
// const result = JSON.parse(decodeURIComponent(res))
// console.log(result)
if (searchResultKey === 'general_infos') {
//
state.isShowSearchRecordDetailInfo = false
@ -722,6 +742,8 @@ const handleEnterSearchResultChat = () => {
trigger="click"
:options="state.chatSearchOptions"
style="width: 248px; height: 677px;"
:show="state.showSearchDropdown"
@clickoutside="state.showSearchDropdown = false"
>
<n-input
placeholder="搜索好友 / 群聊"
@ -729,6 +751,7 @@ const handleEnterSearchResultChat = () => {
round
clearable
style="width: 78%;"
@click="state.showSearchDropdown = true"
>
<template #prefix>
<n-icon :component="Search" />
@ -950,6 +973,7 @@ const handleEnterSearchResultChat = () => {
@clickStayItemChange="handleClickStayItemChange"
@lastIdChange="handleSearchListLastIdChange"
:searchResultMaxHeight="'517px'"
:selectItemInList="state.selectItemInList"
></chatAppSearchList>
</div>
<div class="search-record-detail">

View File

@ -47,6 +47,7 @@ interface State {
currentReadDetailPage: number
hasMoreReadListDetail: boolean
loadingReadListDetail: boolean
lastScrollTop: number
}
const props = defineProps({
@ -76,7 +77,7 @@ const props = defineProps({
}
})
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(props.uid)
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid)
const uploadsStore = useUploadsStore()
const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
@ -125,8 +126,16 @@ const onPanelScroll = (e: any) => {
}
const height = e.target.scrollTop + e.target.clientHeight
const scrollHeight = e.target.scrollHeight
const isScrollingDown = e.target.scrollTop > state.value.lastScrollTop
state.value.lastScrollTop = e.target.scrollTop
skipBottom.value = height < e.target.scrollHeight - 200
//
if (height === scrollHeight && isScrollingDown) {
onLoadMoreDown()
}
skipBottom.value = height < scrollHeight - 200
if (!skipBottom.value && dialogueStore.unreadBubble) {
dialogueStore.setUnreadBubble(0)
}
@ -400,7 +409,8 @@ const state = ref<State>({
currentMsgReadDetail: null,
currentReadDetailPage: 1,
hasMoreReadListDetail: true,
loadingReadListDetail: false
loadingReadListDetail: false,
lastScrollTop: 0
})
//