diff --git a/src/api/im/friend/request/index.ts b/src/api/im/friend/request/index.ts index 73cb801dd..900ea77cf 100644 --- a/src/api/im/friend/request/index.ts +++ b/src/api/im/friend/request/index.ts @@ -45,7 +45,22 @@ export const refuseFriendRequest = (id: number | string, handleContent?: string) }) } -// 查询「我相关」的好友申请列表(含我发起的、别人加我的) -export const getMyFriendRequestList = () => { - return request.get({ url: '/im/friend-request/list' }) +// 查询「我相关」的好友申请列表(游标分页:传 lastRequestId 加载更多) +export const getMyFriendRequestList = (limit: number, lastRequestId?: number) => { + const params: Record = { limit } + if (lastRequestId != null) { + params.lastRequestId = lastRequestId + } + return request.get({ + url: '/im/friend-request/list', + params + }) +} + +// 按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用) +export const getMyFriendRequest = (id: number) => { + return request.get({ + url: '/im/friend-request/get', + params: { id } + }) } diff --git a/src/views/im/home/pages/contact/FriendRequestList.vue b/src/views/im/home/pages/contact/FriendRequestList.vue index 9e9e39271..c111e981c 100644 --- a/src/views/im/home/pages/contact/FriendRequestList.vue +++ b/src/views/im/home/pages/contact/FriendRequestList.vue @@ -62,6 +62,14 @@ > 暂无新的朋友 + +
+ {{ loadingMore ? '加载中…' : '加载更多' }} +
@@ -108,4 +116,17 @@ const enrichedRequests = computed(() => 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 + } +} diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts index 1e8893703..f44676c18 100644 --- a/src/views/im/home/store/friendStore.ts +++ b/src/views/im/home/store/friendStore.ts @@ -16,11 +16,16 @@ import { agreeFriendRequest as apiAgreeFriendRequest, refuseFriendRequest as apiRefuseFriendRequest, getMyFriendRequestList as apiGetMyFriendRequestList, + getMyFriendRequest as apiGetMyFriendRequest, type ImFriendRequestApplyReqVO, type ImFriendRequestRespVO } from '@/api/im/friend/request' 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 { getFriendDisplayName } from '../../utils/user' import type { Friend, FriendRequest } from '../types' @@ -29,6 +34,8 @@ import type { Friend, FriendRequest } from '../types' let pendingFetchFriends: Promise | null = null /** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */ let pendingFetchRequests: Promise | null = null +/** 当前正在进行的「加载更多申请」请求 */ +let pendingLoadMoreRequests: Promise | null = null /** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */ export interface FriendNotificationPayload { @@ -61,8 +68,10 @@ export const useFriendStore = defineStore('imFriendStore', { friends: [] as Friend[], // 仅 fetchFriends 成功后置位;loadFriends(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过 loaded: false, - /** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */ - friendRequests: [] as FriendRequest[] + /** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序游标分页) */ + friendRequests: [] as FriendRequest[], + /** 是否还有更早的申请记录可加载;返回不满 page size 即置 false */ + hasMoreFriendRequests: true }), getters: { @@ -191,9 +200,8 @@ export const useFriendStore = defineStore('imFriendStore', { request.handleResult = ImFriendRequestHandleResult.AGREED request.handleTime = Date.now() } else { - // 列表过期场景兜底重拉 - // TODO @AI:是不是只拉这个人?避免拉所有? - await this.fetchFriendRequests() + // 本地列表没这条,按 id 单查兜底 + await this.loadFriendRequest(requestId) } }, @@ -206,18 +214,21 @@ export const useFriendStore = defineStore('imFriendStore', { request.handleContent = handleContent request.handleTime = Date.now() } else { - await this.fetchFriendRequests() + await this.loadFriendRequest(requestId) } }, - /** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */ + /** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */ async fetchFriendRequests() { if (pendingFetchRequests) { return pendingFetchRequests } - pendingFetchRequests = apiGetMyFriendRequestList() + pendingFetchRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE) .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(() => { pendingFetchRequests = null @@ -225,11 +236,47 @@ export const useFriendStore = defineStore('imFriendStore', { 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 索引 */ findFriendRequest(requestId: number): FriendRequest | undefined { 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;级联清理本地私聊会话) */ @@ -364,8 +411,8 @@ export const useFriendStore = defineStore('imFriendStore', { request.handleResult = ImFriendRequestHandleResult.AGREED request.handleTime = Date.now() } else { - // 本地列表可能未初始化(例如刚登录还没进 contact 页),兜底重拉 - void this.fetchFriendRequests() + // 本地列表可能没这条(例如刚登录还没进 contact 页),按 id 单查兜底 + void this.loadFriendRequest(payload.requestId!) } }, @@ -377,7 +424,7 @@ export const useFriendStore = defineStore('imFriendStore', { request.handleContent = payload.handleContent request.handleTime = Date.now() } else { - void this.fetchFriendRequests() + void this.loadFriendRequest(payload.requestId!) } }, @@ -442,6 +489,7 @@ export const useFriendStore = defineStore('imFriendStore', { this.friends = [] this.friendRequests = [] this.loaded = false + this.hasMoreFriendRequests = true } } }) diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index b11538a17..ba589ee11 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -141,6 +141,9 @@ export const PRIVATE_MESSAGE_PULL_SIZE = 100 /** 每次拉取群聊消息的最大条数(后端上限 1000,前端取保守值 100) */ export const GROUP_MESSAGE_PULL_SIZE = 100 +/** 「我相关」好友申请列表的单次拉取条数(游标分页 page size,前端控制) */ +export const FRIEND_REQUEST_PAGE_SIZE = 100 + /** 消息之间渲染「时间分隔条」的阈值:10 分钟 */ export const TIME_TIP_GAP_MS = 10 * 60 * 1000