fix(chat): loadInitial/loadTalkerName 加请求序号防会话切换竞态(ChatMain+ChatTimeline同修)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 18:20:14 +08:00
parent 3aaf4ec8fa
commit 91639b28d8
2 changed files with 37 additions and 3 deletions
+20 -2
View File
@@ -41,6 +41,12 @@ const total = ref(0)
/** 首次加载是否完成(msgid 定位只在已加载数据上尝试,避免误报「不在当前页」)。 */
const initialLoaded = ref(false)
// 请求序号防竞态:快速切换会话时,旧会话的请求仍在途、响应后到,若不拦截会把
// 新会话已渲染的 messages/total/currentPage/标题 覆盖回旧会话数据(自愈但错乱)。
// 每次 loadInitial/loadTalkerName 自增取号,响应回来时号不对即整体丢弃。
let loadSeq = 0
let nameSeq = 0
const isGroup = computed(() => props.kind === 'group')
/** 是否已选定具体会话(决定是否拉消息 / 显示空态)。账号级入口(talkerId='')→ 空态。 */
const hasActive = computed(() => !!props.talkerId)
@@ -62,12 +68,14 @@ const visibleMessages = computed(() => {
const talkerName = ref<string | null>(null)
async function loadTalkerName(): Promise<void> {
const seq = ++nameSeq
talkerName.value = null
if (!hasActive.value) return
try {
if (isGroup.value && props.talkerId) {
// 群名(room_id 精确单行直查,不再拉整页群列表)
const res = await getGroups({ channel: CHANNEL, room_id: props.talkerId, page: 1, page_size: 1 })
if (seq !== nameSeq) return // 竞态:已有更新的请求,丢弃旧会话响应
talkerName.value = res.items[0]?.room_name || null
} else if (props.talkerId) {
// 客户昵称
@@ -78,6 +86,7 @@ async function loadTalkerName(): Promise<void> {
page: 1,
page_size: 1,
})
if (seq !== nameSeq) return // 竞态:已有更新的请求,丢弃旧会话响应
const c = res.items[0]
talkerName.value = c?.friend_nickname || c?.friend_remark || null
}
@@ -184,6 +193,7 @@ function queryFor(pageNum: number) {
* returns asc, so the last page holds the newest slice.
*/
async function loadInitial(): Promise<void> {
const seq = ++loadSeq
highlightId.value = null
// 账号级入口(无具体会话):不拉消息,清空状态,留空态给右列。
if (!hasActive.value) {
@@ -193,12 +203,15 @@ async function loadInitial(): Promise<void> {
currentPage.value = 1
pendingMsgid.value = null
initialLoaded.value = true
// 本分支已取最新序号,在途旧请求的 finally 会被拦下,须自行复位加载态
loading.value = false
return
}
loading.value = true
errorMsg.value = null
try {
const first = await getMessages(queryFor(1))
if (seq !== loadSeq) return // 竞态:已有更新的加载,丢弃旧会话响应
total.value = first.total
totalPages.value = Math.max(1, Math.ceil(first.total / PAGE_SIZE))
@@ -206,16 +219,21 @@ async function loadInitial(): Promise<void> {
messages.value = first.items
} else {
const last = await getMessages(queryFor(totalPages.value))
if (seq !== loadSeq) return // 竞态:已有更新的加载,丢弃旧会话响应
messages.value = last.items
}
currentPage.value = totalPages.value
scrollToBottom()
} catch (e) {
if (seq !== loadSeq) return // 竞态:旧请求的报错不展示(会话已切换)
errorMsg.value = (e as Error).message
messages.value = []
} finally {
loading.value = false
initialLoaded.value = true
// 只有最新请求才能复位加载态:被取代的旧请求不得清掉新请求的 loading
if (seq === loadSeq) {
loading.value = false
initialLoaded.value = true
}
}
// DOM 渲染完再尝试 msgid 定位(覆盖 scrollToBottom 的置底)。
nextTick(() => tryLocate())
+17 -1
View File
@@ -42,6 +42,12 @@ const currentPage = ref(1)
const totalPages = ref(1)
const total = ref(0)
// 请求序号防竞态:快速切换会话时,旧会话的请求仍在途、响应后到,若不拦截会把
// 新会话已渲染的 messages/total/currentPage/标题 覆盖回旧会话数据(自愈但错乱)。
// 每次 loadInitial/loadTalkerName 自增取号,响应回来时号不对即整体丢弃。
let loadSeq = 0
let nameSeq = 0
const channelTyped = computed(() => props.channel as Channel)
/** True when this timeline is a group chat (drives the query + title). */
@@ -72,12 +78,14 @@ const visibleMessages = computed(() => {
const talkerName = ref<string | null>(null)
async function loadTalkerName(): Promise<void> {
const seq = ++nameSeq
talkerName.value = null
if (!hasActive.value) return
try {
if (isGroup.value && props.roomId) {
// 群名(room_id 精确单行直查,不再拉整页群列表)
const res = await getGroups({ channel: channelTyped.value, room_id: props.roomId, page: 1, page_size: 1 })
if (seq !== nameSeq) return // 竞态:已有更新的请求,丢弃旧会话响应
talkerName.value = res.items[0]?.room_name || null
} else if (props.talkerId) {
// 客户昵称
@@ -88,6 +96,7 @@ async function loadTalkerName(): Promise<void> {
page: 1,
page_size: 1,
})
if (seq !== nameSeq) return // 竞态:已有更新的请求,丢弃旧会话响应
const c = res.items[0]
talkerName.value = c?.friend_nickname || c?.friend_remark || null
}
@@ -188,18 +197,22 @@ function queryFor(pageNum: number) {
* returns asc, so the last page holds the newest slice.
*/
async function loadInitial(): Promise<void> {
const seq = ++loadSeq
// 账号级入口(无具体会话):不拉消息,清空状态,留空态给右侧主区。
if (!hasActive.value) {
messages.value = []
total.value = 0
totalPages.value = 1
currentPage.value = 1
// 本分支已取最新序号,在途旧请求的 finally 会被拦下,须自行复位加载态
loading.value = false
return
}
loading.value = true
errorMsg.value = null
try {
const first = await getMessages(queryFor(1))
if (seq !== loadSeq) return // 竞态:已有更新的加载,丢弃旧会话响应
total.value = first.total
totalPages.value = Math.max(1, Math.ceil(first.total / PAGE_SIZE))
@@ -207,15 +220,18 @@ async function loadInitial(): Promise<void> {
messages.value = first.items
} else {
const last = await getMessages(queryFor(totalPages.value))
if (seq !== loadSeq) return // 竞态:已有更新的加载,丢弃旧会话响应
messages.value = last.items
}
currentPage.value = totalPages.value
scrollToBottom()
} catch (e) {
if (seq !== loadSeq) return // 竞态:旧请求的报错不展示(会话已切换)
errorMsg.value = (e as Error).message
messages.value = []
} finally {
loading.value = false
// 只有最新请求才能复位加载态:被取代的旧请求不得清掉新请求的 loading
if (seq === loadSeq) loading.value = false
}
}