♻️ refactor(im):用户申请列表,增加流式查询,避免一次性加载过多,或者历史无法被加载到。
parent
9fc25b7109
commit
14e3f85cb0
|
|
@ -45,7 +45,22 @@ export const refuseFriendRequest = (id: number | string, handleContent?: string)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询「我相关」的好友申请列表(含我发起的、别人加我的)
|
// 查询「我相关」的好友申请列表(游标分页:传 lastRequestId 加载更多)
|
||||||
export const getMyFriendRequestList = () => {
|
export const getMyFriendRequestList = (limit: number, lastRequestId?: number) => {
|
||||||
return request.get<ImFriendRequestRespVO[]>({ url: '/im/friend-request/list' })
|
const params: Record<string, number> = { limit }
|
||||||
|
if (lastRequestId != null) {
|
||||||
|
params.lastRequestId = lastRequestId
|
||||||
|
}
|
||||||
|
return request.get<ImFriendRequestRespVO[]>({
|
||||||
|
url: '/im/friend-request/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)
|
||||||
|
export const getMyFriendRequest = (id: number) => {
|
||||||
|
return request.get<ImFriendRequestRespVO | null>({
|
||||||
|
url: '/im/friend-request/get',
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@
|
||||||
>
|
>
|
||||||
暂无新的朋友
|
暂无新的朋友
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 加载更多:按本地最旧 requestId 游标分页拉下一批;hasMore=false 不展示 -->
|
||||||
|
<div
|
||||||
|
v-else-if="friendStore.hasMoreFriendRequests"
|
||||||
|
class="py-2 text-12px text-center cursor-pointer text-[var(--el-text-color-secondary)] hover:bg-[var(--el-fill-color-light)]"
|
||||||
|
@click="handleLoadMore"
|
||||||
|
>
|
||||||
|
{{ loadingMore ? '加载中…' : '加载更多' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -108,4 +116,17 @@ const enrichedRequests = computed(() =>
|
||||||
props.requests.map((request) => ({ request, peer: getPeer(request) }))
|
props.requests.map((request) => ({ request, peer: getPeer(request) }))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 点击「加载更多」拉下一页;store 内部按 lastRequestId 游标分页 + pending 去重 */
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
async function handleLoadMore() {
|
||||||
|
if (loadingMore.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingMore.value = true
|
||||||
|
try {
|
||||||
|
await friendStore.loadMoreFriendRequests()
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,16 @@ import {
|
||||||
agreeFriendRequest as apiAgreeFriendRequest,
|
agreeFriendRequest as apiAgreeFriendRequest,
|
||||||
refuseFriendRequest as apiRefuseFriendRequest,
|
refuseFriendRequest as apiRefuseFriendRequest,
|
||||||
getMyFriendRequestList as apiGetMyFriendRequestList,
|
getMyFriendRequestList as apiGetMyFriendRequestList,
|
||||||
|
getMyFriendRequest as apiGetMyFriendRequest,
|
||||||
type ImFriendRequestApplyReqVO,
|
type ImFriendRequestApplyReqVO,
|
||||||
type ImFriendRequestRespVO
|
type ImFriendRequestRespVO
|
||||||
} from '@/api/im/friend/request'
|
} from '@/api/im/friend/request'
|
||||||
import { useConversationStore } from './conversationStore'
|
import { useConversationStore } from './conversationStore'
|
||||||
import { ImConversationType, ImFriendRequestHandleResult } from '../../utils/constants'
|
import {
|
||||||
|
FRIEND_REQUEST_PAGE_SIZE,
|
||||||
|
ImConversationType,
|
||||||
|
ImFriendRequestHandleResult
|
||||||
|
} from '../../utils/constants'
|
||||||
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
|
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
|
||||||
import { getFriendDisplayName } from '../../utils/user'
|
import { getFriendDisplayName } from '../../utils/user'
|
||||||
import type { Friend, FriendRequest } from '../types'
|
import type { Friend, FriendRequest } from '../types'
|
||||||
|
|
@ -29,6 +34,8 @@ import type { Friend, FriendRequest } from '../types'
|
||||||
let pendingFetchFriends: Promise<void> | null = null
|
let pendingFetchFriends: Promise<void> | null = null
|
||||||
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */
|
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */
|
||||||
let pendingFetchRequests: Promise<void> | null = null
|
let pendingFetchRequests: Promise<void> | null = null
|
||||||
|
/** 当前正在进行的「加载更多申请」请求 */
|
||||||
|
let pendingLoadMoreRequests: Promise<void> | null = null
|
||||||
|
|
||||||
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
|
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
|
||||||
export interface FriendNotificationPayload {
|
export interface FriendNotificationPayload {
|
||||||
|
|
@ -61,8 +68,10 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
friends: [] as Friend[],
|
friends: [] as Friend[],
|
||||||
// 仅 fetchFriends 成功后置位;loadFriends(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过
|
// 仅 fetchFriends 成功后置位;loadFriends(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过
|
||||||
loaded: false,
|
loaded: false,
|
||||||
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */
|
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序游标分页) */
|
||||||
friendRequests: [] as FriendRequest[]
|
friendRequests: [] as FriendRequest[],
|
||||||
|
/** 是否还有更早的申请记录可加载;返回不满 page size 即置 false */
|
||||||
|
hasMoreFriendRequests: true
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
|
@ -191,9 +200,8 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||||
request.handleTime = Date.now()
|
request.handleTime = Date.now()
|
||||||
} else {
|
} else {
|
||||||
// 列表过期场景兜底重拉
|
// 本地列表没这条,按 id 单查兜底
|
||||||
// TODO @AI:是不是只拉这个人?避免拉所有?
|
await this.loadFriendRequest(requestId)
|
||||||
await this.fetchFriendRequests()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -206,18 +214,21 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
request.handleContent = handleContent
|
request.handleContent = handleContent
|
||||||
request.handleTime = Date.now()
|
request.handleTime = Date.now()
|
||||||
} else {
|
} else {
|
||||||
await this.fetchFriendRequests()
|
await this.loadFriendRequest(requestId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */
|
/** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */
|
||||||
async fetchFriendRequests() {
|
async fetchFriendRequests() {
|
||||||
if (pendingFetchRequests) {
|
if (pendingFetchRequests) {
|
||||||
return pendingFetchRequests
|
return pendingFetchRequests
|
||||||
}
|
}
|
||||||
pendingFetchRequests = apiGetMyFriendRequestList()
|
pendingFetchRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE)
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
this.friendRequests = (list || []).map(convertFriendRequest)
|
const items = (list || []).map(convertFriendRequest)
|
||||||
|
this.friendRequests = items
|
||||||
|
// 不足一页即没有更多;满页可能还有,等 loadMore 拉到 0 条再确定
|
||||||
|
this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
pendingFetchRequests = null
|
pendingFetchRequests = null
|
||||||
|
|
@ -225,11 +236,47 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
return pendingFetchRequests
|
return pendingFetchRequests
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 加载更多申请(按本地最旧 requestId 游标分页);无更多 / pending 中直接返回 */
|
||||||
|
async loadMoreFriendRequests() {
|
||||||
|
if (!this.hasMoreFriendRequests || pendingLoadMoreRequests || pendingFetchRequests) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const oldest = this.friendRequests[this.friendRequests.length - 1]
|
||||||
|
if (!oldest) {
|
||||||
|
return this.fetchFriendRequests()
|
||||||
|
}
|
||||||
|
pendingLoadMoreRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE, oldest.id)
|
||||||
|
.then((list) => {
|
||||||
|
const items = (list || []).map(convertFriendRequest)
|
||||||
|
this.friendRequests.push(...items)
|
||||||
|
this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pendingLoadMoreRequests = null
|
||||||
|
})
|
||||||
|
return pendingLoadMoreRequests
|
||||||
|
},
|
||||||
|
|
||||||
/** 按 id 查申请记录;列表是按 id 倒序的小列表,O(n) find 即可,不再维护 Map 索引 */
|
/** 按 id 查申请记录;列表是按 id 倒序的小列表,O(n) find 即可,不再维护 Map 索引 */
|
||||||
findFriendRequest(requestId: number): FriendRequest | undefined {
|
findFriendRequest(requestId: number): FriendRequest | undefined {
|
||||||
return this.friendRequests.find((request) => request.id === requestId)
|
return this.friendRequests.find((request) => request.id === requestId)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 按 id 从后端单查并 upsert 到本地(dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
|
||||||
|
async loadFriendRequest(requestId: number) {
|
||||||
|
const data = await apiGetMyFriendRequest(requestId)
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = convertFriendRequest(data)
|
||||||
|
const existing = this.findFriendRequest(requestId)
|
||||||
|
if (existing) {
|
||||||
|
Object.assign(existing, next)
|
||||||
|
} else {
|
||||||
|
this.friendRequests.unshift(next)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== 好友关系操作 ====================
|
// ==================== 好友关系操作 ====================
|
||||||
|
|
||||||
/** 删除好友(单向软删,本端置 DISABLE;级联清理本地私聊会话) */
|
/** 删除好友(单向软删,本端置 DISABLE;级联清理本地私聊会话) */
|
||||||
|
|
@ -364,8 +411,8 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||||
request.handleTime = Date.now()
|
request.handleTime = Date.now()
|
||||||
} else {
|
} else {
|
||||||
// 本地列表可能未初始化(例如刚登录还没进 contact 页),兜底重拉
|
// 本地列表可能没这条(例如刚登录还没进 contact 页),按 id 单查兜底
|
||||||
void this.fetchFriendRequests()
|
void this.loadFriendRequest(payload.requestId!)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -377,7 +424,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
request.handleContent = payload.handleContent
|
request.handleContent = payload.handleContent
|
||||||
request.handleTime = Date.now()
|
request.handleTime = Date.now()
|
||||||
} else {
|
} else {
|
||||||
void this.fetchFriendRequests()
|
void this.loadFriendRequest(payload.requestId!)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -442,6 +489,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
this.friends = []
|
this.friends = []
|
||||||
this.friendRequests = []
|
this.friendRequests = []
|
||||||
this.loaded = false
|
this.loaded = false
|
||||||
|
this.hasMoreFriendRequests = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,9 @@ export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
||||||
/** 每次拉取群聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
/** 每次拉取群聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
||||||
export const GROUP_MESSAGE_PULL_SIZE = 100
|
export const GROUP_MESSAGE_PULL_SIZE = 100
|
||||||
|
|
||||||
|
/** 「我相关」好友申请列表的单次拉取条数(游标分页 page size,前端控制) */
|
||||||
|
export const FRIEND_REQUEST_PAGE_SIZE = 100
|
||||||
|
|
||||||
/** 消息之间渲染「时间分隔条」的阈值:10 分钟 */
|
/** 消息之间渲染「时间分隔条」的阈值:10 分钟 */
|
||||||
export const TIME_TIP_GAP_MS = 10 * 60 * 1000
|
export const TIME_TIP_GAP_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue