mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-05-11 13:17:29 +08:00
Merge pull request #757 from nagisa77/feature/daily_bugfix_0827
fix: 站内信 scroll问题 #749
This commit is contained in:
@@ -113,17 +113,23 @@ const error = ref(null)
|
|||||||
const conversationId = route.params.id
|
const conversationId = route.params.id
|
||||||
const currentUser = ref(null)
|
const currentUser = ref(null)
|
||||||
const messagesListEl = ref(null)
|
const messagesListEl = ref(null)
|
||||||
let lastMessageEl = null
|
|
||||||
const currentPage = ref(0)
|
const currentPage = ref(0)
|
||||||
const totalPages = ref(0)
|
const totalPages = ref(0)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
let scrollInterval = null
|
|
||||||
const conversationName = ref('')
|
const conversationName = ref('')
|
||||||
const isChannel = ref(false)
|
const isChannel = ref(false)
|
||||||
const isFloatMode = computed(() => route.query.float !== undefined)
|
const isFloatMode = computed(() => route.query.float !== undefined)
|
||||||
const floatRoute = useState('messageFloatRoute')
|
const floatRoute = useState('messageFloatRoute')
|
||||||
const replyTo = ref(null)
|
const replyTo = ref(null)
|
||||||
|
|
||||||
|
const isUserNearBottom = ref(true)
|
||||||
|
function updateNearBottom() {
|
||||||
|
const el = messagesListEl.value
|
||||||
|
if (!el) return
|
||||||
|
const threshold = 40 // px
|
||||||
|
isUserNearBottom.value = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold
|
||||||
|
}
|
||||||
|
|
||||||
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
const hasMoreMessages = computed(() => currentPage.value < totalPages.value - 1)
|
||||||
|
|
||||||
const otherParticipant = computed(() => {
|
const otherParticipant = computed(() => {
|
||||||
@@ -133,20 +139,37 @@ const otherParticipant = computed(() => {
|
|||||||
return participants.value.find((p) => p.id !== currentUser.value.id)
|
return participants.value.find((p) => p.id !== currentUser.value.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
function isSentByCurrentUser(message) {
|
|
||||||
return message.sender.id === currentUser.value?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAvatarError(event) {
|
|
||||||
event.target.src = '/default-avatar.svg'
|
|
||||||
}
|
|
||||||
|
|
||||||
function setReply(message) {
|
function setReply(message) {
|
||||||
replyTo.value = message
|
replyTo.value = message
|
||||||
}
|
}
|
||||||
|
|
||||||
// No changes needed here, as renderMarkdown is now imported.
|
/** 改造:滚动函数 —— smooth & instant */
|
||||||
// The old function is removed.
|
function scrollToBottomSmooth() {
|
||||||
|
const el = messagesListEl.value
|
||||||
|
if (!el) return
|
||||||
|
// 优先使用原生 smooth,失败则降级
|
||||||
|
try {
|
||||||
|
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
||||||
|
} catch {
|
||||||
|
// 降级:简易动画
|
||||||
|
const start = el.scrollTop
|
||||||
|
const end = el.scrollHeight
|
||||||
|
const duration = 200
|
||||||
|
const startTs = performance.now()
|
||||||
|
function step(now) {
|
||||||
|
const p = Math.min(1, (now - startTs) / duration)
|
||||||
|
el.scrollTop = start + (end - start) * p
|
||||||
|
if (p < 1) requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottomInstant() {
|
||||||
|
const el = messagesListEl.value
|
||||||
|
if (!el) return
|
||||||
|
el.scrollTop = el.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchMessages(page = 0) {
|
async function fetchMessages(page = 0) {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
@@ -181,7 +204,6 @@ async function fetchMessages(page = 0) {
|
|||||||
isChannel.value = conversationData.channel
|
isChannel.value = conversationData.channel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since the backend sorts by descending, we need to reverse for correct chat order
|
|
||||||
const newMessages = pageData.content.reverse().map((item) => ({
|
const newMessages = pageData.content.reverse().map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
src: item.sender.avatar,
|
src: item.sender.avatar,
|
||||||
@@ -202,12 +224,16 @@ async function fetchMessages(page = 0) {
|
|||||||
currentPage.value = pageData.number
|
currentPage.value = pageData.number
|
||||||
totalPages.value = pageData.totalPages
|
totalPages.value = pageData.totalPages
|
||||||
|
|
||||||
// Scrolling is now fully handled by the watcher
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (page > 0 && list) {
|
if (page > 0 && list) {
|
||||||
|
// 加载更多:保持原视口位置
|
||||||
const newScrollHeight = list.scrollHeight
|
const newScrollHeight = list.scrollHeight
|
||||||
list.scrollTop = newScrollHeight - oldScrollHeight
|
list.scrollTop = newScrollHeight - oldScrollHeight
|
||||||
|
} else if (page === 0) {
|
||||||
|
// 首次加载:定位到底部(不用动画,避免“闪动感”)
|
||||||
|
scrollToBottomInstant()
|
||||||
}
|
}
|
||||||
|
updateNearBottom()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message
|
error.value = e.message
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
@@ -272,9 +298,10 @@ async function sendMessage(content, clearInput) {
|
|||||||
})
|
})
|
||||||
clearInput()
|
clearInput()
|
||||||
replyTo.value = null
|
replyTo.value = null
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom()
|
await nextTick()
|
||||||
}, 100)
|
// 仅“发送消息成功后”才平滑滚动到底部
|
||||||
|
scrollToBottomSmooth()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -290,7 +317,6 @@ async function markConversationAsRead() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
// After marking as read, refresh the global unread count
|
|
||||||
refreshGlobalUnreadCount()
|
refreshGlobalUnreadCount()
|
||||||
refreshChannelUnread()
|
refreshChannelUnread()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -298,37 +324,12 @@ async function markConversationAsRead() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
|
||||||
if (messagesListEl.value) {
|
|
||||||
const element = messagesListEl.value
|
|
||||||
// 強制滾動到底部,使用 smooth 行為確保視覺效果
|
|
||||||
element.scrollTop = element.scrollHeight
|
|
||||||
|
|
||||||
// 再次確認滾動位置
|
|
||||||
setTimeout(() => {
|
|
||||||
if (element.scrollTop < element.scrollHeight - element.clientHeight) {
|
|
||||||
element.scrollTop = element.scrollHeight
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
messages,
|
|
||||||
async (newMessages) => {
|
|
||||||
if (newMessages.length === 0) return
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// Simple, reliable scroll to bottom
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, 100)
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 监听列表滚动,实时感知是否接近底部
|
||||||
|
if (messagesListEl.value) {
|
||||||
|
messagesListEl.value.addEventListener('scroll', updateNearBottom, { passive: true })
|
||||||
|
}
|
||||||
|
|
||||||
currentUser.value = await fetchCurrentUser()
|
currentUser.value = await fetchCurrentUser()
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await fetchMessages(0)
|
await fetchMessages(0)
|
||||||
@@ -345,9 +346,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
watch(isConnected, (newValue) => {
|
watch(isConnected, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
// 等待一小段时间确保连接稳定
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
subscription = subscribe(`/topic/conversation/${conversationId}`, (message) => {
|
subscription = subscribe(`/topic/conversation/${conversationId}`, async (message) => {
|
||||||
// 避免重复显示当前用户发送的消息
|
// 避免重复显示当前用户发送的消息
|
||||||
if (message.sender.id !== currentUser.value.id) {
|
if (message.sender.id !== currentUser.value.id) {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
@@ -357,11 +357,10 @@ watch(isConnected, (newValue) => {
|
|||||||
openUser(message.sender.id)
|
openUser(message.sender.id)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// 实时收到消息时自动标记为已读
|
// 收到消息后只标记已读,不强制滚动(符合“非发送不拉底”)
|
||||||
markConversationAsRead()
|
markConversationAsRead()
|
||||||
setTimeout(() => {
|
await nextTick()
|
||||||
scrollToBottom()
|
updateNearBottom()
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 500)
|
}, 500)
|
||||||
@@ -369,23 +368,12 @@ watch(isConnected, (newValue) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
// This will be called every time the component is activated (navigated to)
|
// 返回页面时:刷新数据与已读,不做强制滚动,保持用户当前位置
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await fetchMessages(0)
|
await fetchMessages(0)
|
||||||
await markConversationAsRead()
|
await markConversationAsRead()
|
||||||
|
|
||||||
// 確保滾動到底部 - 使用多重延遲策略
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
setTimeout(() => {
|
updateNearBottom()
|
||||||
scrollToBottom()
|
|
||||||
}, 100)
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, 300)
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
if (!isConnected.value) {
|
if (!isConnected.value) {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token) connect(token)
|
if (token) connect(token)
|
||||||
@@ -406,6 +394,9 @@ onUnmounted(() => {
|
|||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
subscription = null
|
subscription = null
|
||||||
}
|
}
|
||||||
|
if (messagesListEl.value) {
|
||||||
|
messagesListEl.value.removeEventListener('scroll', updateNearBottom)
|
||||||
|
}
|
||||||
disconnect()
|
disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user