perf(component): 优化好友申请列表

This commit is contained in:
Dawn
2025-03-18 02:14:13 +08:00
parent a39c054b62
commit 52f9047299
5 changed files with 182 additions and 80 deletions
+117 -56
View File
@@ -5,63 +5,93 @@
<svg class="size-18px cursor-pointer"><use href="#delete"></use></svg>
</n-flex>
<n-scrollbar id="friend-request-scrollbar" style="max-height: calc(100vh - 80px)">
<n-flex
vertical
:size="10"
class="p-[10px_30px] box-border"
v-for="(item, index) in contactStore.requestFriendsList"
:key="index">
<n-flex
align="center"
justify="space-between"
class="bg-[--center-bg-color] rounded-10px p-20px box-border border-(1px solid [--bg-popover])">
<n-flex align="center" :size="10">
<n-avatar round size="large" :src="avatarSrc(useUserInfo(item.uid).value.avatar!)" class="mr-10px" />
<n-flex vertical :size="12">
<n-flex align="center" :size="10">
<n-popover
@update:show="handlePopoverUpdate(currentUserId, $event)"
trigger="click"
placement="bottom-start"
:show-arrow="false"
style="padding: 0; background: var(--bg-info)">
<template #trigger>
<p @click="currentUserId = item.uid" class="text-(14px #13987f) cursor-pointer">
{{ useUserInfo(item.uid).value.name }}
</p>
</template>
<!-- 用户个人信息框 -->
<InfoPopover v-if="currentUserId === item.uid" :uid="item.uid" />
</n-popover>
<p class="text-(14px [--text-color])">
{{ isCurrentUser(item.uid) ? '正在验证你的邀请' : '请求加为好友' }}
</p>
</n-flex>
<p v-show="item.msg" class="text-(12px [--text-color])">留言{{ item.msg }}</p>
</n-flex>
</n-flex>
<n-virtual-list
style="max-height: calc(100vh - 80px)"
:items="contactStore.requestFriendsList"
:item-size="102"
:item-resizable="true"
@scroll="handleScroll"
ref="virtualListRef">
<template #default="{ item }">
<n-flex vertical :size="10" class="p-[10px_30px] box-border">
<n-flex
align="center"
:size="10"
v-if="item.status === RequestFriendAgreeStatus.Waiting && !isCurrentUser(item.uid)">
<n-button secondary :loading="loadingMap[item.applyId]" @click="handleAgree(item.applyId)">接受</n-button>
<n-dropdown
trigger="click"
:options="dropdownOptions"
@select="(key: string) => handleFriendAction(key, item.applyId)">
<n-icon class="cursor-pointer px-6px">
<svg class="size-16px"><use href="#more"></use></svg>
</n-icon>
</n-dropdown>
justify="space-between"
class="bg-[--center-bg-color] rounded-10px p-20px box-border border-(1px solid [--bg-popover])">
<n-flex align="center" :size="10">
<n-avatar
round
size="large"
:src="avatarSrc(useUserInfo(isCurrentUser(item.uid) ? item.targetId : item.uid).value.avatar!)"
class="mr-10px" />
<n-flex vertical :size="12">
<n-flex align="center" :size="10">
<p
@click="isCurrentUser(item.uid) ? (currentUserId = item.targetId) : (currentUserId = item.uid)"
class="text-(14px #13987f) cursor-pointer">
{{ useUserInfo(isCurrentUser(item.uid) ? item.targetId : item.uid).value.name }}
</p>
<p class="text-(14px [--text-color])">
{{
isCurrentUser(item.uid)
? isAccepted(item.targetId)
? '已同意你的请求'
: '正在验证你的邀请'
: '请求加为好友'
}}
</p>
</n-flex>
<p v-show="item.msg" class="text-(12px [--text-color])">留言{{ item.msg }}</p>
</n-flex>
</n-flex>
<n-flex
align="center"
:size="10"
v-if="item.status === RequestFriendAgreeStatus.Waiting && !isCurrentUser(item.uid)">
<n-button secondary :loading="loadingMap[item.applyId]" @click="handleAgree(item.applyId)">接受</n-button>
<n-dropdown
trigger="click"
:options="dropdownOptions"
@select="(key: string) => handleFriendAction(key, item.applyId)">
<n-icon class="cursor-pointer px-6px">
<svg class="size-16px"><use href="#more"></use></svg>
</n-icon>
</n-dropdown>
</n-flex>
<span class="text-(12px #64a29c)" v-else-if="isCurrentUser(item.uid)">{{
isAccepted(item.targetId) ? '已同意' : '等待验证'
}}</span>
<span class="text-(12px #64a29c)" v-else>已同意</span>
</n-flex>
<span class="text-(12px #64a29c)" v-else-if="isCurrentUser(item.uid)">等待验证</span>
<span class="text-(12px #64a29c)" v-else>已同意</span>
</n-flex>
</n-flex>
</n-scrollbar>
</template>
</n-virtual-list>
<!-- 加载更多提示 -->
<n-flex v-if="contactStore.requestFriendsOptions.isLoading" justify="center" class="py-10px">
<n-spin size="small" />
<span class="text-(12px [--text-color]) ml-8px">加载中...</span>
</n-flex>
<!-- 没有更多数据提示 -->
<n-flex
v-else-if="contactStore.requestFriendsOptions.isLast && contactStore.requestFriendsList.length > 0"
justify="center"
class="py-10px">
<span class="text-(12px [--text-color])">没有更多好友申请了</span>
</n-flex>
<!-- 空数据提示 -->
<n-flex
v-else-if="contactStore.requestFriendsList.length === 0"
vertical
justify="center"
align="center"
class="py-40px">
<n-empty description="暂无好友申请" />
</n-flex>
</n-flex>
</template>
<script setup lang="ts">
@@ -69,15 +99,20 @@ import { useContactStore } from '@/stores/contacts.ts'
import { useUserInfo } from '@/hooks/useCached.ts'
import { RequestFriendAgreeStatus } from '@/services/types.ts'
import { AvatarUtils } from '@/utils/AvatarUtils'
import { usePopover } from '@/hooks/usePopover'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const contactStore = useContactStore()
const currentUserId = ref('0')
const loadingMap = ref<Record<string, boolean>>({})
const { handlePopoverUpdate, enableScroll } = usePopover(currentUserId, 'friend-request-scrollbar')
provide('popoverControls', { enableScroll })
const virtualListRef = ref()
const isLoadingMore = ref(false)
// 检查好友申请是否已被接受
const isAccepted = (targetId: string) => {
// 使用缓存集合快速检查目标用户是否在联系人列表中
return contactStore.contactsList.some((contact) => contact.uid === targetId)
}
// 下拉菜单选项
const dropdownOptions = [
@@ -98,6 +133,32 @@ const isCurrentUser = (uid: string) => {
return uid === userStore.userInfo.uid
}
// 处理滚动事件
const handleScroll = (e: Event) => {
if (isLoadingMore.value) return
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLElement
// 当滚动到距离底部20px以内时触发加载更多
if (scrollHeight - scrollTop - clientHeight < 20) {
loadMoreFriendRequests()
}
}
// 加载更多好友申请
const loadMoreFriendRequests = async () => {
// 如果已经是最后一页或正在加载中,则不再加载
if (contactStore.requestFriendsOptions.isLast || contactStore.requestFriendsOptions.isLoading) {
return
}
isLoadingMore.value = true
try {
await contactStore.getRequestFriendsList(false)
} finally {
isLoadingMore.value = false
}
}
const handleAgree = async (applyId: string) => {
loadingMap.value[applyId] = true
contactStore.onAcceptFriend(applyId).then(() => {
+6
View File
@@ -60,6 +60,8 @@ export type RegisterUserReq = {
export type ListResponse<T> = {
/** 游标(下次翻页带上这参数)*/
cursor: string
/** 当前页数 */
pageNo?: number
/** 是否最后一页 */
isLast: boolean
list: T[]
@@ -436,6 +438,10 @@ export type RequestFriendItem = {
type: number
/** 申请人uid */
uid: string
/** 被申请人id */
targetId: string
/** 申请时间 */
createTime: number
/** 会话 ID */
roomId: string
}
+5 -1
View File
@@ -18,7 +18,6 @@ export default {
markMsg: `${prefix + URLEnum.CHAT}/msg/mark`, // 消息标记
blockUser: `${prefix + URLEnum.USER}/black`, // 拉黑用户
recallMsg: `${prefix + URLEnum.CHAT}/msg/recall`, // 撤回消息
fileUpload: `${prefix + URLEnum.SYSTEM + URLEnum.OSS}/upload/url`, // 文件上传
addEmoji: `${prefix + URLEnum.USER}/emoji`, // 增加表情
deleteEmoji: `${prefix + URLEnum.USER}/emoji`, // 删除表情
getEmoji: `${prefix + URLEnum.USER}/emoji/list`, // 查询表情包
@@ -60,6 +59,11 @@ export default {
searchGroup: `${prefix + URLEnum.ROOM}/search`, // 搜索群聊
applyGroup: `${prefix + URLEnum.ROOM}/applyGroup`, // 申请加群
// -------------- 系统相关 ---------------
fileUpload: `${prefix + URLEnum.SYSTEM + URLEnum.OSS}/upload/url`, // 文件上传
initConfig: `${prefix + URLEnum.SYSTEM}/config/init`, // 获取配置文件
getQiniuToken: `${prefix + URLEnum.SYSTEM}/ossToken`, // 获取七牛云上传token
// -------------- token相关 ---------------
register: `${prefix + URLEnum.TOKEN}/register`, // 注册
login: `${prefix + URLEnum.TOKEN}/login`, // 登录
+39 -23
View File
@@ -18,7 +18,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
/** 联系人列表分页选项 */
const contactsOptions = ref({ isLast: false, isLoading: false, cursor: '' })
/** 好友请求列表分页选项 */
const requestFriendsOptions = ref({ isLast: false, isLoading: false, cursor: '' })
const requestFriendsOptions = ref({ isLast: false, isLoading: false, cursor: '', pageNo: 1 })
/** 群聊列表 */
const groupChatList = ref<GroupListReq[]>([])
@@ -69,13 +69,8 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
* 更新全局store中的未读计数
*/
const getNewFriendCount = async () => {
const res = await apis.newFriendCount().catch(() => {
// 出错时不更新计数
return null
})
const res = await apis.newFriendCount()
if (!res) return
// 更新全局store中的未读计数
globalStore.unReadMark.newFriendUnreadCount = res.unReadCount
}
@@ -89,24 +84,45 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
if (!isFresh) {
if (requestFriendsOptions.value.isLast || requestFriendsOptions.value.isLoading) return
}
// 设置加载状态
requestFriendsOptions.value.isLoading = true
const res = await apis
.requestFriendList({
pageSize: 50,
cursor: isFresh || !requestFriendsOptions.value.cursor ? '' : requestFriendsOptions.value.cursor
// 刷新时重置页码
if (isFresh) {
requestFriendsOptions.value.pageNo = 1
requestFriendsOptions.value.cursor = ''
}
try {
const res = await apis.requestFriendList({
pageNo: requestFriendsOptions.value.pageNo,
pageSize: 30,
cursor: isFresh ? '' : requestFriendsOptions.value.cursor
})
.catch(() => {
requestFriendsOptions.value.isLoading = false
})
if (!res) return
const data = res
// 刷新模式下替换整个列表,否则追加到列表末尾
isFresh
? requestFriendsList.value.splice(0, requestFriendsList.value.length, ...data.list)
: requestFriendsList.value.push(...data.list)
requestFriendsOptions.value.cursor = data.cursor
requestFriendsOptions.value.isLast = data.isLast
requestFriendsOptions.value.isLoading = false
if (!res) return
// 刷新模式下替换整个列表,否则追加到列表末尾
if (isFresh) {
requestFriendsList.value.splice(0, requestFriendsList.value.length, ...res.list)
} else {
requestFriendsList.value.push(...res.list)
}
// 更新分页信息
requestFriendsOptions.value.cursor = res.cursor
requestFriendsOptions.value.isLast = res.isLast
// 如果有返回pageNo,则使用服务器返回的pageNo,否则自增页码
if (res.pageNo) {
requestFriendsOptions.value.pageNo = res.pageNo + 1
} else {
requestFriendsOptions.value.pageNo++
}
} catch (error) {
console.error('获取好友申请列表失败:', error)
} finally {
requestFriendsOptions.value.isLoading = false
}
}
// 初始化时默认执行一次加载
+15
View File
@@ -7,6 +7,10 @@
<div class="text-(14px [--text-color])">好友通知</div>
<n-flex align="center" :size="4">
<n-badge :value="globalStore.unReadMark.newFriendUnreadCount" :max="15" />
<n-badge
v-if="hasPendingFriendRequests && globalStore.unReadMark.newFriendUnreadCount === 0"
dot
color="#d5304f" />
<svg class="size-16px rotate-270 color-[--text-color]"><use href="#down"></use></svg>
</n-flex>
</n-flex>
@@ -104,6 +108,8 @@ import { AvatarUtils } from '@/utils/AvatarUtils'
import { useGlobalStore } from '@/stores/global.ts'
import { useUserStatusStore } from '@/stores/userStatus'
import { storeToRefs } from 'pinia'
import { RequestFriendAgreeStatus } from '@/services/types'
import { useUserStore } from '@/stores/user'
const menuList = ref([
{ label: '添加分组', icon: 'plus' },
@@ -117,7 +123,16 @@ const shrinkStatus = ref(false)
const contactStore = useContactStore()
const globalStore = useGlobalStore()
const userStatusStore = useUserStatusStore()
const userStore = useUserStore()
const { stateList } = storeToRefs(userStatusStore)
/** 是否有待处理的好友申请 */
const hasPendingFriendRequests = computed(() => {
return contactStore.requestFriendsList.some(
(item) => item.status === RequestFriendAgreeStatus.Waiting && item.uid !== userStore.userInfo.uid
)
})
/** 群聊列表 */
const groupChatList = computed(() => {
return [...contactStore.groupChatList].sort((a, b) => {