Compare commits

..

No commits in common. "07c380812219be77a047e9f5925c30745e29a07f" and "7733f88dae83afa0a0b9e80ca398959e4362681a" have entirely different histories.

3 changed files with 31 additions and 142 deletions

View File

@ -83,37 +83,9 @@ export const useTalkRecord = (uid: number) => {
location.msgid = '' location.msgid = ''
location.num = 0 location.num = 0
// 使用更精确的滚动定位方式 element?.scrollIntoView({
const el = document.getElementById('imChatPanel') behavior: 'smooth'
if (el && element) { })
// 计算目标元素相对于容器的偏移量
const targetOffsetTop = element.offsetTop
const containerHeight = el.clientHeight
const targetHeight = element.offsetHeight
// 计算理想的滚动位置:让目标元素居中显示
let scrollTo = targetOffsetTop - containerHeight / 2 + targetHeight / 2
// 边界检查:确保目标元素完全在可视区域内
const minScrollTop = 0
const maxScrollTop = el.scrollHeight - containerHeight
// 如果计算出的位置超出边界,调整到边界内
if (scrollTo < minScrollTop) {
scrollTo = minScrollTop
} else if (scrollTo > maxScrollTop) {
scrollTo = maxScrollTop
}
// 执行滚动
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
} else {
// 降级方案:使用 scrollIntoView
element?.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
}
addClass(element, 'border') addClass(element, 'border')
@ -261,10 +233,8 @@ export const useTalkRecord = (uid: number) => {
// 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置 // 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置
requestAnimationFrame(() => { requestAnimationFrame(() => {
const el = document.getElementById('imChatPanel') const el = document.getElementById('imChatPanel')
const msgId = options.specifiedMsg?.msg_id const target = document.getElementById(options.specifiedMsg?.msg_id || '')
const target = msgId ? document.getElementById(msgId) : null if (el && target) {
if (el) {
// 如果是向上加载更多,保持原有内容位置 // 如果是向上加载更多,保持原有内容位置
if (contextParams.direction === 'up') { if (contextParams.direction === 'up') {
el.scrollTop = el.scrollHeight - scrollHeight el.scrollTop = el.scrollHeight - scrollHeight
@ -276,54 +246,21 @@ export const useTalkRecord = (uid: number) => {
el.scrollTop = scrollHeight - el.clientHeight el.scrollTop = scrollHeight - el.clientHeight
} }
}) })
} else if (target && msgId) {
// 只有在有目标元素且有 msg_id 时才执行定位逻辑
// 如果是定位到特定消息,计算并滚动到目标位置
// 使用 nextTick 确保 DOM 完全渲染后再计算位置
nextTick(() => {
const el = document.getElementById('imChatPanel')
const target = document.getElementById(msgId)
if (el && target) {
loadConfig.isLocatingMessage = true
// 计算目标元素相对于容器的偏移量
const targetOffsetTop = target.offsetTop
const containerHeight = el.clientHeight
const targetHeight = target.offsetHeight
// 计算理想的滚动位置:让目标元素居中显示
let scrollTo = targetOffsetTop - containerHeight / 2 + targetHeight / 2
// 边界检查:确保目标元素完全在可视区域内
const minScrollTop = 0
const maxScrollTop = el.scrollHeight - containerHeight
// 如果计算出的位置超出边界,调整到边界内
if (scrollTo < minScrollTop) {
scrollTo = minScrollTop
} else if (scrollTo > maxScrollTop) {
scrollTo = maxScrollTop
}
// 执行滚动
el.scrollTo({ top: scrollTo, behavior: 'smooth' })
// 添加高亮边框
addClass(target, 'border')
setTimeout(() => removeClass(target, 'border'), 3000)
// 清除 dialogueStore 中的 specifiedMsg避免重复使用
if (dialogueStore.specifiedMsg) {
dialogueStore.specifiedMsg = ''
dialogueStore.noRefreshRecords = true
}
}
})
} else { } else {
// 其他情况滚动到底部 // 如果是定位到特定消息,计算并滚动到目标位置
scrollToBottom() 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
scrollToBottom()
} }
}) })
}) })
@ -348,7 +285,6 @@ export const useTalkRecord = (uid: number) => {
loadConfig.specialParams.receiver_id === loadConfig.receiver_id loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) { ) {
// 特殊参数模式下direction: 'up'cursor: 当前最小 sequence // 特殊参数模式下direction: 'up'cursor: 当前最小 sequence
// 注意:向上加载更多时,不传递 msg_id避免触发定位逻辑
onLoad( onLoad(
{ {
receiver_id: loadConfig.receiver_id, receiver_id: loadConfig.receiver_id,
@ -361,8 +297,10 @@ export const useTalkRecord = (uid: number) => {
direction: 'up', direction: 'up',
sort_sequence: '', sort_sequence: '',
cursor: getMinSequence(), cursor: getMinSequence(),
// 向上加载更多时不传递 msg_id保持原有滚动位置 msg_id:
msg_id: undefined records.value.find((item) =>
item.sequence === getMinSequence() ? item.msg_id : ''
)?.msg_id || ''
} }
} }
) )

View File

@ -37,9 +37,6 @@ export const useDialogueStore = defineStore('dialogue', {
// 查询指定消息上下文的消息信息 // 查询指定消息上下文的消息信息
specifiedMsg: '', specifiedMsg: '',
// 是否不刷新聊天记录
noRefreshRecords: false,
// 是否是手动切换会话 // 是否是手动切换会话
isManualSwitch: false, isManualSwitch: false,

View File

@ -77,9 +77,7 @@ const props = defineProps({
} }
}) })
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord( const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage, onLoadMoreDown } = useTalkRecord(props.uid)
props.uid
)
const uploadsStore = useUploadsStore() const uploadsStore = useUploadsStore()
const { useMessage } = useUtil() const { useMessage } = useUtil()
const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu() const { dropdown, showDropdownMenu, closeDropdownMenu } = useMenu()
@ -301,6 +299,7 @@ const onContextMenu = (e: any, item: ITalkRecord) => {
} }
const onConvertText = async (data: ITalkRecord) => { const onConvertText = async (data: ITalkRecord) => {
data.is_convert_text = 1 data.is_convert_text = 1
const res = await voiceToText({ msgId: data.msg_id, voiceUrl: data.extra.url }) const res = await voiceToText({ msgId: data.msg_id, voiceUrl: data.extra.url })
if (res.code == 200) { if (res.code == 200) {
@ -341,29 +340,12 @@ const onRowClick = (item: ITalkRecord) => {
} }
const lastParams = ref('') const lastParams = ref('')
//
const currentSessionKey = ref('')
//
let noRefreshTimer: number | null = null
// props // props
watch( watch(
() => props, () => props,
async (newProps) => { async (newProps) => {
await nextTick() await nextTick()
//
const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}`
//
const isSessionChanged = currentSessionKey.value !== newSessionKey
// noRefreshRecords
if (isSessionChanged) {
dialogueStore.noRefreshRecords = false
if (noRefreshTimer) {
clearTimeout(noRefreshTimer)
noRefreshTimer = null
}
currentSessionKey.value = newSessionKey
}
let specialParams = undefined let specialParams = undefined
if (newProps.specifiedMsg) { if (newProps.specifiedMsg) {
try { try {
@ -377,21 +359,6 @@ watch(
} }
} catch (e) {} } catch (e) {}
} }
//
if (dialogueStore.noRefreshRecords) {
//
if (noRefreshTimer) {
clearTimeout(noRefreshTimer)
}
//
noRefreshTimer = setTimeout(() => {
dialogueStore.noRefreshRecords = false
noRefreshTimer = null
}, 3000)
return
}
onLoad( onLoad(
{ {
receiver_id: newProps.receiver_id, receiver_id: newProps.receiver_id,
@ -912,20 +879,11 @@ const onCustomSkipBottomEvent = () => {
<!-- 已读回执 --> <!-- 已读回执 -->
<div class="talk_read_num" v-if="item.user_id === props.uid"> <div class="talk_read_num" v-if="item.user_id === props.uid">
<span v-if="props.talk_type === 1">{{ <span v-if="props.talk_type === 1">{{
item.read_total_num > 0 ? '已读' : '未读' item.read_total_num > 0 ? '已读' : '未读'
}}</span> }}</span>
<n-popover <n-popover trigger="click" placement="bottom-end" style="height: 382px; padding: 0;" v-if="props.talk_type === 2">
trigger="click"
placement="bottom-end"
style="height: 382px; padding: 0;"
v-if="props.talk_type === 2"
>
<template #trigger> <template #trigger>
<span <span v-if="props.talk_type === 2" @click="toShowMessageReadDetail(item)" style="cursor: pointer;">
v-if="props.talk_type === 2"
@click="toShowMessageReadDetail(item)"
style="cursor: pointer;"
>
已读 ({{ item?.read_total_num || 0 }}/{{ 已读 ({{ item?.read_total_num || 0 }}/{{
props.num - 1 > 0 ? props.num - 1 : 0 props.num - 1 > 0 ? props.num - 1 : 0
}}) }})
@ -993,11 +951,7 @@ const onCustomSkipBottomEvent = () => {
</div> </div>
<!-- 置底按钮 --> <!-- 置底按钮 -->
<SkipBottom <SkipBottom v-model="skipBottom" :customSkipBottomEvent="true" @customSkipBottomEvent="onCustomSkipBottomEvent"/>
v-model="skipBottom"
:customSkipBottomEvent="true"
@customSkipBottomEvent="onCustomSkipBottomEvent"
/>
</section> </section>
<!-- 右键菜单 --> <!-- 右键菜单 -->