mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-01 14:52:48 +08:00
perf(component): ⚡ 优化好友申请列表
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时默认执行一次加载
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user