修复消息中心滚动显示

This commit is contained in:
jxxghp
2026-06-14 21:32:14 +08:00
parent da0cd14af8
commit 0a7d53b5c7
2 changed files with 64 additions and 31 deletions
@@ -78,7 +78,6 @@ watch(visible, async newValue => {
if (newValue) { if (newValue) {
await nextTick() await nextTick()
messageViewRef.value?.resumeSSE?.() messageViewRef.value?.resumeSSE?.()
messageViewRef.value?.forceScrollToEnd?.()
clearUnreadMessageState() clearUnreadMessageState()
@@ -90,8 +89,6 @@ watch(visible, async newValue => {
onMounted(async () => { onMounted(async () => {
await nextTick() await nextTick()
messageViewRef.value?.resumeSSE?.()
messageViewRef.value?.forceScrollToEnd?.()
clearUnreadMessageState() clearUnreadMessageState()
}) })
+55 -19
View File
@@ -41,6 +41,7 @@ const MESSAGE_AUTO_SCROLL_THRESHOLD = 64
let scrollTimer: number | undefined let scrollTimer: number | undefined
let scrollReleaseTimer: number | undefined let scrollReleaseTimer: number | undefined
let boundScrollContainer: HTMLElement | null = null
// 生成消息去重签名 // 生成消息去重签名
// SSE 消息只有 date 没有 reg_time,数据库消息只有 reg_time 没有 date // SSE 消息只有 date 没有 reg_time,数据库消息只有 reg_time 没有 date
@@ -83,10 +84,34 @@ function updateLastTime(message: Message) {
} }
} }
function getScrollContainer() { /** 判断元素自身是否是真正承载滚动的位置。 */
const container = messageListRef.value?.$el ?? messageListRef.value function isScrollableElement(element: HTMLElement) {
const { overflowY } = window.getComputedStyle(element)
const canScroll = overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay'
return container instanceof HTMLElement ? container : null return canScroll && element.scrollHeight > element.clientHeight + 1
}
/** 获取消息列表所在的真实滚动容器。 */
function getScrollContainer() {
const element = messageListRef.value?.$el ?? messageListRef.value
if (!(element instanceof HTMLElement)) {
return null
}
let container: HTMLElement | null = element
while (container) {
if (isScrollableElement(container)) {
return container
}
container = container.parentElement
}
const dialogCardText = element.closest('.v-card-text')
return dialogCardText instanceof HTMLElement ? dialogCardText : element
} }
function isNearBottom(container: HTMLElement) { function isNearBottom(container: HTMLElement) {
@@ -114,23 +139,31 @@ function bindScrollListener() {
return return
} }
if (boundScrollContainer && boundScrollContainer !== container) {
boundScrollContainer.removeEventListener('scroll', handleScroll)
}
container.removeEventListener('scroll', handleScroll) container.removeEventListener('scroll', handleScroll)
container.addEventListener('scroll', handleScroll, { passive: true }) container.addEventListener('scroll', handleScroll, { passive: true })
boundScrollContainer = container
updateAutoScrollState() updateAutoScrollState()
} }
function unbindScrollListener() { function unbindScrollListener() {
getScrollContainer()?.removeEventListener('scroll', handleScroll) boundScrollContainer?.removeEventListener('scroll', handleScroll)
boundScrollContainer = null
} }
function scrollContainerToEnd() { /** 滚动到底部,并在布局稳定前连续几帧校正滚动位置。 */
function scrollContainerToEnd(retryCount = 1) {
const container = getScrollContainer() const container = getScrollContainer()
if (!container) { if (!container) {
return return
} }
bindScrollListener()
isSyncingScroll.value = true isSyncingScroll.value = true
container.scrollTop = container.scrollHeight container.scrollTop = Math.max(0, container.scrollHeight - container.clientHeight)
requestAnimationFrame(() => { requestAnimationFrame(() => {
const latestContainer = getScrollContainer() const latestContainer = getScrollContainer()
@@ -139,9 +172,14 @@ function scrollContainerToEnd() {
return return
} }
latestContainer.scrollTop = latestContainer.scrollHeight latestContainer.scrollTop = Math.max(0, latestContainer.scrollHeight - latestContainer.clientHeight)
shouldAutoScroll.value = true shouldAutoScroll.value = true
if (retryCount > 0) {
scrollContainerToEnd(retryCount - 1)
return
}
if (scrollReleaseTimer) { if (scrollReleaseTimer) {
window.clearTimeout(scrollReleaseTimer) window.clearTimeout(scrollReleaseTimer)
} }
@@ -165,7 +203,7 @@ function requestScrollToEnd(force = false) {
scrollTimer = window.setTimeout(() => { scrollTimer = window.setTimeout(() => {
nextTick(() => { nextTick(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
scrollContainerToEnd() scrollContainerToEnd(force ? 6 : 1)
}) })
}) })
}, force ? 0 : 80) }, force ? 0 : 80)
@@ -231,6 +269,8 @@ async function loadMessages({ done }: { done: any }) {
try { try {
// 设置加载中 // 设置加载中
loading.value = true loading.value = true
const isFirstPage = page.value === 1
currData.value = await api.get('message/web', { currData.value = await api.get('message/web', {
params: { params: {
page: page.value, page: page.value,
@@ -240,16 +280,17 @@ async function loadMessages({ done }: { done: any }) {
// 已加载过 // 已加载过
isLoaded.value = true isLoaded.value = true
if (currData.value.length > 0) { if (currData.value.length > 0) {
const hasNewMessage = mergeMessages(currData.value) mergeMessages(currData.value)
// 首次加载时滚动到底部
if (page.value === 1 && hasNewMessage) {
requestScrollToEnd(true)
}
// 页码+1 // 页码+1
page.value++ page.value++
// 完成 // 完成
done('ok') done('ok')
// 首次加载完成后再滚动,避免列表尚未完成布局时滚动失效。
if (isFirstPage) {
requestScrollToEnd(true)
}
} else { } else {
// 没有新数据 // 没有新数据
done('empty') done('empty')
@@ -341,7 +382,6 @@ defineExpose({
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
bindScrollListener() bindScrollListener()
requestScrollToEnd(true)
}) })
}) })
@@ -372,10 +412,8 @@ onBeforeUnmount(() => {
<LoadingBanner /> <LoadingBanner />
</template> </template>
<template #empty> {{ t('message.noMoreData') }} </template> <template #empty> {{ t('message.noMoreData') }} </template>
<VVirtualScroll renderless :items="messages" :item-height="160">
<template #default="{ item, index, itemRef }">
<div <div
:ref="itemRef" v-for="(item, index) in messages"
:key="getMessageKey(item) || index" :key="getMessageKey(item) || index"
class="chat-group d-flex mt-5 mb-8" class="chat-group d-flex mt-5 mb-8"
:class="item.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'" :class="item.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'"
@@ -384,7 +422,5 @@ onBeforeUnmount(() => {
<MessageCard :message="item" @imageload="handleImageLoad" /> <MessageCard :message="item" @imageload="handleImageLoad" />
</div> </div>
</div> </div>
</template>
</VVirtualScroll>
</VInfiniteScroll> </VInfiniteScroll>
</template> </template>