diff --git a/src/components/dialog/ShortcutMessageDialog.vue b/src/components/dialog/ShortcutMessageDialog.vue
index d4e8b05f..c5f16d54 100644
--- a/src/components/dialog/ShortcutMessageDialog.vue
+++ b/src/components/dialog/ShortcutMessageDialog.vue
@@ -78,7 +78,6 @@ watch(visible, async newValue => {
if (newValue) {
await nextTick()
messageViewRef.value?.resumeSSE?.()
- messageViewRef.value?.forceScrollToEnd?.()
clearUnreadMessageState()
@@ -90,8 +89,6 @@ watch(visible, async newValue => {
onMounted(async () => {
await nextTick()
- messageViewRef.value?.resumeSSE?.()
- messageViewRef.value?.forceScrollToEnd?.()
clearUnreadMessageState()
})
diff --git a/src/views/system/MessageView.vue b/src/views/system/MessageView.vue
index 43e29b11..ea92c5ea 100644
--- a/src/views/system/MessageView.vue
+++ b/src/views/system/MessageView.vue
@@ -41,6 +41,7 @@ const MESSAGE_AUTO_SCROLL_THRESHOLD = 64
let scrollTimer: number | undefined
let scrollReleaseTimer: number | undefined
+let boundScrollContainer: HTMLElement | null = null
// 生成消息去重签名
// 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) {
@@ -114,23 +139,31 @@ function bindScrollListener() {
return
}
+ if (boundScrollContainer && boundScrollContainer !== container) {
+ boundScrollContainer.removeEventListener('scroll', handleScroll)
+ }
+
container.removeEventListener('scroll', handleScroll)
container.addEventListener('scroll', handleScroll, { passive: true })
+ boundScrollContainer = container
updateAutoScrollState()
}
function unbindScrollListener() {
- getScrollContainer()?.removeEventListener('scroll', handleScroll)
+ boundScrollContainer?.removeEventListener('scroll', handleScroll)
+ boundScrollContainer = null
}
-function scrollContainerToEnd() {
+/** 滚动到底部,并在布局稳定前连续几帧校正滚动位置。 */
+function scrollContainerToEnd(retryCount = 1) {
const container = getScrollContainer()
if (!container) {
return
}
+ bindScrollListener()
isSyncingScroll.value = true
- container.scrollTop = container.scrollHeight
+ container.scrollTop = Math.max(0, container.scrollHeight - container.clientHeight)
requestAnimationFrame(() => {
const latestContainer = getScrollContainer()
@@ -139,9 +172,14 @@ function scrollContainerToEnd() {
return
}
- latestContainer.scrollTop = latestContainer.scrollHeight
+ latestContainer.scrollTop = Math.max(0, latestContainer.scrollHeight - latestContainer.clientHeight)
shouldAutoScroll.value = true
+ if (retryCount > 0) {
+ scrollContainerToEnd(retryCount - 1)
+ return
+ }
+
if (scrollReleaseTimer) {
window.clearTimeout(scrollReleaseTimer)
}
@@ -165,7 +203,7 @@ function requestScrollToEnd(force = false) {
scrollTimer = window.setTimeout(() => {
nextTick(() => {
requestAnimationFrame(() => {
- scrollContainerToEnd()
+ scrollContainerToEnd(force ? 6 : 1)
})
})
}, force ? 0 : 80)
@@ -231,6 +269,8 @@ async function loadMessages({ done }: { done: any }) {
try {
// 设置加载中
loading.value = true
+ const isFirstPage = page.value === 1
+
currData.value = await api.get('message/web', {
params: {
page: page.value,
@@ -240,16 +280,17 @@ async function loadMessages({ done }: { done: any }) {
// 已加载过
isLoaded.value = true
if (currData.value.length > 0) {
- const hasNewMessage = mergeMessages(currData.value)
+ mergeMessages(currData.value)
- // 首次加载时滚动到底部
- if (page.value === 1 && hasNewMessage) {
- requestScrollToEnd(true)
- }
// 页码+1
page.value++
// 完成
done('ok')
+
+ // 首次加载完成后再滚动,避免列表尚未完成布局时滚动失效。
+ if (isFirstPage) {
+ requestScrollToEnd(true)
+ }
} else {
// 没有新数据
done('empty')
@@ -341,7 +382,6 @@ defineExpose({
onMounted(() => {
nextTick(() => {
bindScrollListener()
- requestScrollToEnd(true)
})
})
@@ -372,19 +412,15 @@ onBeforeUnmount(() => {