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