chat-pc/src/hooks/useTalkRecord.ts

386 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { reactive, computed, nextTick } from 'vue'
import { ServeTalkRecords } from '@/api/chat'
import { useDialogueStore } from '@/store'
import { ITalkRecord } from '@/types/chat'
import { formatTalkRecord } from '@/utils/talk'
import { addClass, removeClass, scrollToBottom, isScrollAtBottom } from '@/utils/dom'
interface Params {
receiver_id: number
talk_type: number
limit: number
}
interface SpecialParams extends Params {
msg_id?: string
cursor?: number
direction?: 'up' | 'down'
sort_sequence?: string
type?: 'loadMore'
}
interface LoadOptions {
specifiedMsg?: SpecialParams
middleMsgCreatedAt?: string
}
export const useTalkRecord = (uid: number) => {
const dialogueStore = useDialogueStore()
const records = computed((): ITalkRecord[] => dialogueStore.records)
const location = reactive({
msgid: '',
num: 0
})
const loadConfig = reactive({
receiver_id: 0,
talk_type: 0,
status: 0,
cursor: 0,
specialParams: undefined as SpecialParams | undefined,
isLocatingMessage: false,
isLoadingMore: false,
lastLoadMoreTime: 0,
targetMessagePosition: 0
})
// 重置 loadConfig
const resetLoadConfig = () => {
loadConfig.receiver_id = 0
loadConfig.talk_type = 0
loadConfig.status = 0
loadConfig.cursor = 0
loadConfig.specialParams = undefined
}
const onJumpMessage = (msgid: string) => {
const element = document.getElementById(msgid)
if (!element) {
if (location.msgid == '') {
location.msgid = msgid
location.num = 3
} else {
location.num--
if (location.num === 0) {
location.msgid = ''
location.num = 0
window['$message'].info('仅支持查看最近300条的记录')
return
}
}
const el = document.getElementById('imChatPanel')
return el?.scrollTo({
top: 0,
behavior: 'smooth'
})
}
location.msgid = ''
location.num = 0
element?.scrollIntoView({
behavior: 'smooth'
})
addClass(element, 'border')
setTimeout(() => {
element && removeClass(element, 'border')
}, 3000)
}
// 加载数据列表
const load = async (params: Params) => {
const request = {
talk_type: params.talk_type,
receiver_id: params.receiver_id,
cursor: loadConfig.cursor,
limit: 30
}
loadConfig.status = 0
let scrollHeight = 0
console.log('加载数据列表load')
const el = document.getElementById('imChatPanel')
if (el) {
scrollHeight = el.scrollHeight
}
const { data, code } = await ServeTalkRecords(request)
if (code != 200) {
return (loadConfig.status = 1)
}
// 防止对话切换过快,数据渲染错误
if (
request.talk_type != loadConfig.talk_type ||
request.receiver_id != loadConfig.receiver_id
) {
return (location.msgid = '')
}
const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item))
if (request.cursor == 0) {
// 判断是否是初次加载
dialogueStore.clearDialogueRecord()
}
dialogueStore.unshiftDialogueRecord(items.reverse())
loadConfig.status = items.length >= request.limit ? 1 : 2
loadConfig.cursor = data.cursor
nextTick(() => {
const el = document.getElementById('imChatPanel')
if (el) {
if (request.cursor == 0) {
// el.scrollTop = el.scrollHeight
// setTimeout(() => {
// el.scrollTop = el.scrollHeight + 1000
// }, 500)
scrollToBottom()
} else {
el.scrollTop = el.scrollHeight - scrollHeight
}
}
if (location.msgid) {
onJumpMessage(location.msgid)
}
})
}
// 获取当前消息的最小 sequence
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))
}
// 获取当前消息的最大 sequence
const getMaxSequence = () => {
if (!records.value.length) return 0
return Math.max(...records.value.map((item) => item.sequence))
}
/**
* 加载数据主入口,支持指定消息定位模式
* @param params 原有参数
* @param options 可选,{ specifiedMsg } 指定消息对象
*/
const onLoad = (params: Params, options?: LoadOptions) => {
// 如果会话切换,重置所有状态
if (
params.talk_type !== loadConfig.talk_type ||
params.receiver_id !== loadConfig.receiver_id
) {
resetLoadConfig()
}
loadConfig.cursor = 0
loadConfig.receiver_id = params.receiver_id
loadConfig.talk_type = params.talk_type
console.error('onLoad', params, options)
// 新增:支持指定消息定位模式,参数以传入为准合并
if (options?.specifiedMsg?.cursor !== undefined) {
loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用
console.error('options', options)
loadConfig.status = 0 // 复用主流程 loading 状态
// 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖)
const contextParams = {
...params,
...options.specifiedMsg
}
//msg_id是用来做定位的不做参数所以这里清空
contextParams.msg_id = ''
ServeTalkRecords(contextParams).then(({ data, code }) => {
if (code !== 200) {
loadConfig.status = 2
return
}
// 记录当前滚动高度
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))
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
// 使用 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
scrollToBottom()
}
})
})
return
}
loadConfig.specialParams = undefined // 普通模式清空
// 原有逻辑
load(params)
}
// 向上加载更多(兼容特殊参数模式)
const onRefreshLoad = () => {
console.error('loadConfig.status', loadConfig.status)
if (loadConfig.status == 1) {
console.log('specialParams', loadConfig.specialParams)
// 判断是否是特殊参数模式
if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') {
// 检查特殊参数是否与当前会话匹配
if (
loadConfig.specialParams.talk_type === loadConfig.talk_type &&
loadConfig.specialParams.receiver_id === loadConfig.receiver_id
) {
// 特殊参数模式下direction: 'up'cursor: 当前最小 sequence
onLoad(
{
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
},
{
specifiedMsg: {
...loadConfig.specialParams,
direction: 'up',
sort_sequence: '',
cursor: getMinSequence(),
msg_id:
records.value.find((item) =>
item.sequence === getMinSequence() ? item.msg_id : ''
)?.msg_id || ''
}
}
)
} 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
})
}
}
}
// 向下加载更多(特殊参数模式才生效,普通模式无效,因为普通模式的数据就是从最新开始加载历史的,所以不需要加载更新的数据)
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
) {
onLoad(
{
receiver_id: loadConfig.receiver_id,
talk_type: loadConfig.talk_type,
limit: 30
},
{
specifiedMsg: {
...loadConfig.specialParams,
direction: 'down',
sort_sequence: 'asc',
cursor: getMaxSequence(),
type: 'loadMore'
}
}
)
}
}
}
return {
loadConfig,
records,
onLoad,
onRefreshLoad,
onLoadMoreDown,
onJumpMessage,
resetLoadConfig
}
}