1441 lines
33 KiB
Vue
1441 lines
33 KiB
Vue
<template>
|
||
<view class="friends-content">
|
||
<!-- 背景图片 - 使用mainpage -->
|
||
<image class="background-image" src="/static/background/mainpage.png" mode="aspectFill"></image>
|
||
|
||
<!-- 蒙层 -->
|
||
<view class="background-overlay"></view>
|
||
|
||
<!-- 页面内容 -->
|
||
<view class="content-wrapper">
|
||
<!-- Tab标签页 -->
|
||
<view class="tab-container">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ 'active': activeTab === 0 }"
|
||
:style="{ zIndex: activeTab === 0 ? 4 : 2 }"
|
||
@click="switchTab(0)"
|
||
>
|
||
<text class="tab-text">好友列表</text>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ 'active': activeTab === 1 }"
|
||
:style="{ zIndex: activeTab === 1 ? 4 : 2 }"
|
||
@click="switchTab(1)"
|
||
>
|
||
<text class="tab-text">添加好友</text>
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ 'active': activeTab === 2 }"
|
||
:style="{ zIndex: activeTab === 2 ? 4 : 1 }"
|
||
@click="switchTab(2)"
|
||
>
|
||
<text class="tab-text">好友请求</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 好友列表Tab内容 -->
|
||
<view v-show="activeTab === 0" class="friend-list-container">
|
||
<!-- 大卡片容器 -->
|
||
<view class="friend-list-card">
|
||
<!-- 大卡片背景 -->
|
||
<image class="card-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
|
||
|
||
<!-- 大卡片蒙层 -->
|
||
<view class="card-overlay"></view>
|
||
|
||
<!-- 卡片内容 -->
|
||
<view class="card-content">
|
||
<!-- 好友总数 -->
|
||
<view class="friend-count">
|
||
<text class="count-text">好友列表 ({{ friendList.length }}/{{ totalCount }})</text>
|
||
</view>
|
||
|
||
<scroll-view
|
||
class="scroll-view"
|
||
scroll-y
|
||
:show-scrollbar="false"
|
||
:lower-threshold="50"
|
||
@scrolltolower="onScrollToLower"
|
||
>
|
||
<view class="friend-list">
|
||
<view
|
||
v-for="friend in friendList"
|
||
:key="friend.user_id"
|
||
class="friend-card"
|
||
>
|
||
<!-- 好友头像 -->
|
||
<view class="friend-avatar">
|
||
<Avatar
|
||
:userId="friend.user_id"
|
||
:nickname="friend.nickname"
|
||
:avatarUrl="friend.avatar_url"
|
||
:size="100"
|
||
:borderWidth="4"
|
||
:showLevel="true"
|
||
:level="friend.fan_level"
|
||
:enableCache="false"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 好友信息 -->
|
||
<view class="friend-info">
|
||
<text class="friend-nickname">{{ friend.nickname }}</text>
|
||
<text class="friend-uid">UID: {{ friend.user_id }}</text>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="friend-actions">
|
||
<view class="action-item">
|
||
<view class="action-btn delete-btn" @click="handleDelete(friend)">
|
||
<image class="action-icon" src="/static/icon/reject.png" mode="aspectFit"></image>
|
||
</view>
|
||
<text class="action-label">删除好友</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载状态 -->
|
||
<view v-if="loading" class="loading-more">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
<view v-if="!hasMore && friendList.length > 0" class="no-more">
|
||
<text class="no-more-text">没有更多数据了</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 添加好友Tab内容 -->
|
||
<view v-show="activeTab === 1" class="add-friend-container">
|
||
<!-- 大卡片容器 -->
|
||
<view class="friend-list-card">
|
||
<!-- 大卡片背景 -->
|
||
<image class="card-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
|
||
|
||
<!-- 大卡片蒙层 -->
|
||
<view class="card-overlay"></view>
|
||
|
||
<!-- 卡片内容 -->
|
||
<view class="card-content add-friend-card-content">
|
||
<!-- 搜索框 -->
|
||
<view class="search-wrapper">
|
||
<image class="search-icon" src="/static/icon/search.png" mode="aspectFit"></image>
|
||
<input
|
||
class="search-input"
|
||
v-model="searchUid"
|
||
placeholder="输入uid进行用户搜索"
|
||
type="number"
|
||
/>
|
||
<view
|
||
v-show="searchUid"
|
||
class="clear-btn"
|
||
@click="clearSearch"
|
||
>
|
||
<image class="clear-icon" src="/static/icon/cancel.png" mode="aspectFit"></image>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索结果区域 -->
|
||
<view class="search-result-area" v-if="searchUid">
|
||
<!-- 搜索中 -->
|
||
<view v-if="isSearching" class="search-status">
|
||
<text class="status-text">搜索中...</text>
|
||
</view>
|
||
|
||
<!-- 搜索错误 -->
|
||
<view v-else-if="searchError" class="search-status">
|
||
<text class="error-text">{{ searchError }}</text>
|
||
</view>
|
||
|
||
<!-- 搜索结果卡片 -->
|
||
<view v-else-if="searchResult" class="friend-card">
|
||
<view class="friend-avatar">
|
||
<Avatar
|
||
:userId="searchResult.user_id"
|
||
:nickname="searchResult.nickname"
|
||
:avatarUrl="searchResult.avatar_url"
|
||
:size="100"
|
||
:borderWidth="4"
|
||
:showLevel="true"
|
||
:level="searchResult.fan_level"
|
||
:enableCache="false"
|
||
/>
|
||
</view>
|
||
<view class="friend-info">
|
||
<text class="friend-nickname">{{ searchResult.nickname }}</text>
|
||
<text class="friend-uid">UID: {{ searchResult.user_id }}</text>
|
||
</view>
|
||
<view class="friend-actions">
|
||
<button class="add-friend-btn" @click="handleAddFriend(searchResult.user_id, searchResult.nickname)">
|
||
添加好友
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分隔线 -->
|
||
<view class="divider"></view>
|
||
|
||
<!-- 已发送的好友请求 -->
|
||
<view class="sent-requests-section">
|
||
<view class="section-header">
|
||
<text class="section-title">我发出的好友请求</text>
|
||
</view>
|
||
|
||
<!-- 加载中 -->
|
||
<view v-if="loadingSentRequests" class="loading-status">
|
||
<text class="status-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-else-if="sentRequests.length === 0" class="empty-status">
|
||
<text class="empty-text">暂无已发送的好友请求</text>
|
||
</view>
|
||
|
||
<!-- 请求列表 -->
|
||
<view v-else class="request-list">
|
||
<view
|
||
v-for="request in sentRequests"
|
||
:key="request.to_user_id"
|
||
class="request-card"
|
||
>
|
||
<view class="friend-avatar">
|
||
<Avatar
|
||
:userId="request.to_user_id"
|
||
:nickname="request.to_user_nickname"
|
||
:avatarUrl="request.to_user_avatar_url"
|
||
:size="100"
|
||
:borderWidth="4"
|
||
:showLevel="true"
|
||
:level="request.to_user_fan_level"
|
||
:enableCache="false"
|
||
/>
|
||
</view>
|
||
<view class="request-info">
|
||
<text class="friend-nickname">{{ request.to_user_nickname }}</text>
|
||
<text class="friend-uid">UID: {{ request.to_user_id }}</text>
|
||
<text v-if="request.message" class="request-message">留言: {{ request.message }}</text>
|
||
</view>
|
||
<view class="status-badge" :class="'status-' + request.status">
|
||
<text class="status-text">{{ getStatusText(request.status) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 好友请求Tab内容 -->
|
||
<view v-show="activeTab === 2" class="friend-request-container">
|
||
<!-- 大卡片容器 -->
|
||
<view class="friend-list-card">
|
||
<!-- 大卡片背景 -->
|
||
<image class="card-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
|
||
|
||
<!-- 大卡片蒙层 -->
|
||
<view class="card-overlay"></view>
|
||
|
||
<!-- 卡片内容 -->
|
||
<view class="card-content add-friend-card-content">
|
||
|
||
<!-- 加载中 -->
|
||
<view v-if="loadingReceivedRequests && receivedRequests.length === 0" class="loading-status">
|
||
<text class="status-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-else-if="receivedRequests.length === 0 && !loadingReceivedRequests" class="empty-status">
|
||
<text class="empty-text">暂无收到的好友请求</text>
|
||
</view>
|
||
|
||
<!-- 请求列表 -->
|
||
<scroll-view
|
||
v-else
|
||
class="scroll-view"
|
||
scroll-y
|
||
:show-scrollbar="false"
|
||
:lower-threshold="50"
|
||
@scrolltolower="loadMoreReceivedRequests"
|
||
>
|
||
<view class="request-list">
|
||
<view
|
||
v-for="request in receivedRequests"
|
||
:key="request.request_id || request.from_user_id"
|
||
class="request-card"
|
||
>
|
||
<view class="friend-avatar">
|
||
<Avatar
|
||
:userId="request.from_user_id"
|
||
:nickname="request.from_user_nickname"
|
||
:avatarUrl="request.from_user_avatar_url"
|
||
:size="100"
|
||
:borderWidth="4"
|
||
:showLevel="true"
|
||
:level="request.from_user_fan_level"
|
||
:enableCache="false"
|
||
/>
|
||
</view>
|
||
<view class="request-info">
|
||
<text class="friend-nickname">{{ request.from_user_nickname }}</text>
|
||
<text class="friend-uid">UID: {{ request.from_user_id }}</text>
|
||
<text v-if="request.message" class="request-message">留言: {{ request.message }}</text>
|
||
</view>
|
||
<view class="request-actions">
|
||
<view class="action-btn accept-btn" @click="handleAcceptRequest(request)">
|
||
<image class="action-icon" src="/static/icon/accept.png" mode="aspectFit"></image>
|
||
</view>
|
||
<view class="action-btn reject-btn" @click="handleRejectRequest(request)">
|
||
<image class="action-icon" src="/static/icon/reject.png" mode="aspectFit"></image>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="loadingReceivedRequests" class="loading-more">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
<view v-if="!hasMoreReceivedRequests && receivedRequests.length > 0" class="no-more">
|
||
<text class="no-more-text">没有更多数据了</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 添加好友确认弹窗 -->
|
||
<view class="custom-modal" v-if="showAddFriendModal" @click="cancelAddFriend">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-title">添加好友</view>
|
||
<view class="modal-message">
|
||
确定要添加{{ pendingAddUserNickname ? ` "${pendingAddUserNickname}" ` : '该用户' }}为好友吗?
|
||
</view>
|
||
<view class="modal-buttons">
|
||
<button class="modal-btn modal-btn-cancel" @click="cancelAddFriend">取消</button>
|
||
<button class="modal-btn modal-btn-confirm" @click="confirmAddFriend">确认</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 删除好友确认弹窗 -->
|
||
<view class="custom-modal" v-if="showDeleteFriendModal" @click="cancelDeleteFriend">
|
||
<view class="modal-content" @click.stop>
|
||
<view class="modal-title">确认删除</view>
|
||
<view class="modal-message">
|
||
确定要删除好友"{{ pendingDeleteFriend?.nickname }}"吗?
|
||
</view>
|
||
<view class="modal-buttons">
|
||
<button class="modal-btn modal-btn-cancel" @click="cancelDeleteFriend">取消</button>
|
||
<button class="modal-btn modal-btn-confirm" @click="confirmDeleteFriend">确认</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, watch } from 'vue';
|
||
import Avatar from './Avatar.vue';
|
||
import { friendListApi, searchUserApi, getSentFriendRequestsApi, sendFriendRequestApi, getReceivedFriendRequestsApi, handleFriendRequestApi, deleteFriendApi } from '@/utils/api';
|
||
import { getFriendAvatarRealUrl } from '@/utils/assetImageHelper.js';
|
||
|
||
// Tab状态
|
||
const activeTab = ref(0);
|
||
|
||
// 好友列表数据
|
||
const friendList = ref([]);
|
||
const totalCount = ref(0);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(10);
|
||
const loading = ref(false);
|
||
const hasMore = ref(true);
|
||
|
||
// 搜索UID
|
||
const searchUid = ref('');
|
||
|
||
// 搜索相关
|
||
const searchResult = ref(null); // 搜索结果用户
|
||
const searchError = ref(''); // 搜索错误信息
|
||
const isSearching = ref(false); // 搜索中状态
|
||
let searchTimer = null; // 搜索防抖定时器
|
||
|
||
// 已发送的好友请求
|
||
const sentRequests = ref([]); // 已发送请求列表
|
||
const loadingSentRequests = ref(false);
|
||
|
||
// 收到的好友请求
|
||
const receivedRequests = ref([]);
|
||
const receivedRequestsPage = ref(1);
|
||
const receivedRequestsPageSize = ref(10);
|
||
const loadingReceivedRequests = ref(false);
|
||
const hasMoreReceivedRequests = ref(true);
|
||
|
||
// 添加好友确认弹窗
|
||
const showAddFriendModal = ref(false);
|
||
const pendingAddUserId = ref(null);
|
||
const pendingAddUserNickname = ref('');
|
||
|
||
// 删除好友确认弹窗
|
||
const showDeleteFriendModal = ref(false);
|
||
const pendingDeleteFriend = ref(null);
|
||
|
||
// 切换Tab
|
||
const switchTab = (index) => {
|
||
activeTab.value = index;
|
||
};
|
||
|
||
|
||
// 加载好友列表
|
||
const loadFriendList = async (page) => {
|
||
if (loading.value) return;
|
||
|
||
loading.value = true;
|
||
try {
|
||
const res = await friendListApi(page, pageSize.value);
|
||
if (res.code === 200 && res.data) {
|
||
// 映射字段并解析头像URL
|
||
const friendsPromises = (res.data.items || []).map(async item => {
|
||
|
||
const realAvatarUrl = item.friend_avatar
|
||
? await getFriendAvatarRealUrl(item.friend_avatar)
|
||
: '';
|
||
|
||
|
||
return {
|
||
user_id: item.friend_id,
|
||
nickname: item.friend_nickname,
|
||
fan_level: item.friend_fan_level,
|
||
avatar_url: realAvatarUrl,
|
||
_loading: false // 标记加载完成
|
||
};
|
||
});
|
||
|
||
// 先设置占位数据(用于显示加载状态)
|
||
if (page === 1) {
|
||
const placeholders = (res.data.items || []).map(item => ({
|
||
user_id: item.friend_id,
|
||
nickname: item.friend_nickname,
|
||
fan_level: item.friend_fan_level,
|
||
avatar_url: '',
|
||
_loading: true // 标记正在加载
|
||
}));
|
||
friendList.value = placeholders;
|
||
totalCount.value = res.data.total || 0;
|
||
}
|
||
|
||
// 等待所有头像解析完成
|
||
const mappedList = await Promise.all(friendsPromises);
|
||
|
||
if (page === 1) {
|
||
// 第一页,替换数据
|
||
friendList.value = mappedList;
|
||
} else {
|
||
// 后续页,追加数据
|
||
friendList.value = [...friendList.value, ...mappedList];
|
||
}
|
||
|
||
// 判断是否还有更多数据
|
||
hasMore.value = mappedList.length === pageSize.value &&
|
||
(page * pageSize.value) < totalCount.value;
|
||
currentPage.value = page;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载好友列表失败:', error);
|
||
uni.showToast({
|
||
title: '加载失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 加载更多
|
||
const loadMore = () => {
|
||
if (!hasMore.value || loading.value) return;
|
||
loadFriendList(currentPage.value + 1);
|
||
};
|
||
|
||
// 滚动到底部
|
||
const onScrollToLower = () => {
|
||
if (activeTab.value === 0) {
|
||
loadMore();
|
||
}
|
||
};
|
||
|
||
// 删除好友
|
||
const handleDelete = (friend) => {
|
||
pendingDeleteFriend.value = friend;
|
||
showDeleteFriendModal.value = true;
|
||
};
|
||
|
||
// 取消删除好友
|
||
const cancelDeleteFriend = () => {
|
||
showDeleteFriendModal.value = false;
|
||
pendingDeleteFriend.value = null;
|
||
};
|
||
|
||
// 确认删除好友
|
||
const confirmDeleteFriend = async () => {
|
||
if (!pendingDeleteFriend.value) return;
|
||
|
||
const friend = pendingDeleteFriend.value;
|
||
const friendUserId = friend.user_id; // 使用user_id作为friend_user_id
|
||
|
||
try {
|
||
uni.showLoading({ title: '删除中...', mask: true });
|
||
await deleteFriendApi(friendUserId);
|
||
uni.hideLoading();
|
||
|
||
// 关闭弹窗
|
||
showDeleteFriendModal.value = false;
|
||
|
||
// 从列表中移除
|
||
const index = friendList.value.findIndex(item => item.user_id === friend.user_id);
|
||
if (index !== -1) {
|
||
friendList.value.splice(index, 1);
|
||
totalCount.value = Math.max(0, totalCount.value - 1);
|
||
}
|
||
|
||
uni.showToast({ title: '删除成功', icon: 'success' });
|
||
|
||
// 重置状态
|
||
pendingDeleteFriend.value = null;
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '删除失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 清空搜索框
|
||
const clearSearch = () => {
|
||
searchUid.value = '';
|
||
};
|
||
|
||
// 监听搜索框变化,实现自动搜索
|
||
watch(searchUid, async (newValue) => {
|
||
// 清除之前的定时器
|
||
if (searchTimer) {
|
||
clearTimeout(searchTimer);
|
||
}
|
||
|
||
// 清空之前的结果
|
||
searchResult.value = null;
|
||
searchError.value = '';
|
||
|
||
// 如果搜索框为空,直接返回
|
||
if (!newValue || newValue.trim() === '') {
|
||
return;
|
||
}
|
||
|
||
// 设置新的定时器,500ms 后执行搜索
|
||
searchTimer = setTimeout(async () => {
|
||
try {
|
||
isSearching.value = true;
|
||
const res = await searchUserApi(newValue.trim());
|
||
|
||
if (res.code === 200 && res.data) {
|
||
// 解析头像URL
|
||
const realAvatarUrl = res.data.avatar_url
|
||
? await getFriendAvatarRealUrl(res.data.avatar_url)
|
||
: '';
|
||
searchResult.value = {
|
||
...res.data,
|
||
avatar_url: realAvatarUrl
|
||
};
|
||
searchError.value = '';
|
||
}
|
||
} catch (error) {
|
||
// 处理 404 错误
|
||
if (error.statusCode === 404 || error.message.includes('404')) {
|
||
searchError.value = '用户不存在';
|
||
searchResult.value = null;
|
||
} else {
|
||
searchError.value = error.message || '搜索失败';
|
||
searchResult.value = null;
|
||
}
|
||
} finally {
|
||
isSearching.value = false;
|
||
}
|
||
}, 500);
|
||
});
|
||
|
||
// 添加好友
|
||
const handleAddFriend = (userId, nickname) => {
|
||
pendingAddUserId.value = userId;
|
||
pendingAddUserNickname.value = nickname || '';
|
||
showAddFriendModal.value = true;
|
||
};
|
||
|
||
// 取消添加好友
|
||
const cancelAddFriend = () => {
|
||
showAddFriendModal.value = false;
|
||
pendingAddUserId.value = null;
|
||
pendingAddUserNickname.value = '';
|
||
};
|
||
|
||
// 确认添加好友
|
||
const confirmAddFriend = async () => {
|
||
if (!pendingAddUserId.value) return;
|
||
|
||
try {
|
||
uni.showLoading({ title: '发送中...', mask: true });
|
||
await sendFriendRequestApi(pendingAddUserId.value);
|
||
uni.hideLoading();
|
||
|
||
// 关闭弹窗
|
||
showAddFriendModal.value = false;
|
||
|
||
uni.showToast({ title: '好友请求已发送', icon: 'success' });
|
||
|
||
// 刷新已发送列表
|
||
await fetchSentRequests();
|
||
|
||
// 清空搜索
|
||
searchUid.value = '';
|
||
|
||
// 重置状态
|
||
pendingAddUserId.value = null;
|
||
pendingAddUserNickname.value = '';
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '发送失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取已发送的好友请求列表
|
||
const fetchSentRequests = async () => {
|
||
try {
|
||
loadingSentRequests.value = true;
|
||
const res = await getSentFriendRequestsApi(1, 10);
|
||
|
||
if (res.code === 200) {
|
||
// 解析头像URL
|
||
const requestsPromises = (res.data.items || []).map(async item => {
|
||
const realAvatarUrl = item.to_user_avatar_url
|
||
? await getFriendAvatarRealUrl(item.to_user_avatar_url)
|
||
: '';
|
||
return {
|
||
...item,
|
||
to_user_avatar_url: realAvatarUrl
|
||
};
|
||
});
|
||
sentRequests.value = await Promise.all(requestsPromises);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取已发送请求失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '获取失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
loadingSentRequests.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status) => {
|
||
const statusMap = {
|
||
'pending': '待处理',
|
||
'accepted': '已接受',
|
||
'rejected': '已拒绝',
|
||
'cancelled': '已取消'
|
||
};
|
||
return statusMap[status] || status;
|
||
};
|
||
|
||
// 加载收到的好友请求
|
||
const loadReceivedRequests = async (page) => {
|
||
if (loadingReceivedRequests.value) return;
|
||
|
||
loadingReceivedRequests.value = true;
|
||
try {
|
||
const res = await getReceivedFriendRequestsApi(page, receivedRequestsPageSize.value);
|
||
|
||
if (res.code === 200 && res.data) {
|
||
// 解析头像URL
|
||
const requestsPromises = (res.data.items || []).map(async item => {
|
||
const realAvatarUrl = item.from_user_avatar_url
|
||
? await getFriendAvatarRealUrl(item.from_user_avatar_url)
|
||
: '';
|
||
return {
|
||
...item,
|
||
from_user_avatar_url: realAvatarUrl
|
||
};
|
||
});
|
||
|
||
const items = await Promise.all(requestsPromises);
|
||
|
||
if (page === 1) {
|
||
receivedRequests.value = items;
|
||
} else {
|
||
receivedRequests.value = [...receivedRequests.value, ...items];
|
||
}
|
||
|
||
// 判断是否还有更多
|
||
hasMoreReceivedRequests.value = items.length === receivedRequestsPageSize.value;
|
||
receivedRequestsPage.value = page;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取收到的好友请求失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '获取失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
loadingReceivedRequests.value = false;
|
||
}
|
||
};
|
||
|
||
// 加载更多收到的好友请求
|
||
const loadMoreReceivedRequests = () => {
|
||
if (!hasMoreReceivedRequests.value || loadingReceivedRequests.value) return;
|
||
loadReceivedRequests(receivedRequestsPage.value + 1);
|
||
};
|
||
|
||
// 处理接受好友请求
|
||
const handleAcceptRequest = async (request) => {
|
||
if (!request.request_id) {
|
||
uni.showToast({ title: '请求ID不存在', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: '处理中...', mask: true });
|
||
await handleFriendRequestApi(request.request_id, 'accept');
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '已接受好友请求', icon: 'success' });
|
||
|
||
// 从列表中移除该请求
|
||
const index = receivedRequests.value.findIndex(item =>
|
||
(item.request_id || item.from_user_id) === (request.request_id || request.from_user_id)
|
||
);
|
||
if (index !== -1) {
|
||
receivedRequests.value.splice(index, 1);
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '处理失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理拒绝好友请求
|
||
const handleRejectRequest = async (request) => {
|
||
if (!request.request_id) {
|
||
uni.showToast({ title: '请求ID不存在', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: '处理中...', mask: true });
|
||
await handleFriendRequestApi(request.request_id, 'reject');
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '已拒绝好友请求', icon: 'success' });
|
||
|
||
// 从列表中移除该请求
|
||
const index = receivedRequests.value.findIndex(item =>
|
||
(item.request_id || item.from_user_id) === (request.request_id || request.from_user_id)
|
||
);
|
||
if (index !== -1) {
|
||
receivedRequests.value.splice(index, 1);
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '处理失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 监听 activeTab 变化,切换到添加好友 tab 时刷新
|
||
watch(activeTab, (newTab) => {
|
||
if (newTab === 0) {
|
||
// 切换到好友列表tab时重新加载
|
||
loadFriendList(1);
|
||
} else if (newTab === 1) {
|
||
fetchSentRequests();
|
||
} else if (newTab === 2) {
|
||
// 切换到好友请求tab时加载
|
||
if (receivedRequests.value.length === 0) {
|
||
loadReceivedRequests(1);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 页面加载时获取第一页数据
|
||
onMounted(() => {
|
||
loadFriendList(1);
|
||
// 根据初始tab加载对应数据
|
||
if (activeTab.value === 1) {
|
||
fetchSentRequests();
|
||
} else if (activeTab.value === 2) {
|
||
loadReceivedRequests(1);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.friends-content {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.background-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
object-fit: cover;
|
||
min-width: 100%;
|
||
min-height: 100%;
|
||
filter: blur(20rpx);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.background-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.content-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-top: 192rpx;
|
||
padding-left: 40rpx;
|
||
padding-right: 40rpx;
|
||
padding-bottom: 160rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Tab标签页样式 */
|
||
.tab-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 0;
|
||
margin-bottom: -20rpx;
|
||
align-items: flex-end;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 20rpx 40rpx;
|
||
cursor: pointer;
|
||
border-radius: 30rpx 30rpx 0 0;
|
||
background: linear-gradient(to bottom, rgb(232, 180, 217) 0%, rgb(180, 232, 232) 100%);
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
min-width: 140rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1;
|
||
margin-left: -20rpx;
|
||
transform: translateY(-10rpx);
|
||
}
|
||
|
||
.tab-item:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.tab-item.active {
|
||
background: linear-gradient(to bottom, #E8B4D9 0%, #B4E8E8 100%);
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
|
||
transform: translateY(-15rpx);
|
||
padding-bottom: 40rpx;
|
||
min-width: 160rpx;
|
||
}
|
||
|
||
.tab-item:active {
|
||
transform: translateY(-10rpx) scale(0.95);
|
||
}
|
||
|
||
.tab-item.active:active {
|
||
transform: translateY(-15rpx) scale(0.98);
|
||
}
|
||
|
||
.tab-text {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
font-weight: bold;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.tab-item.active .tab-text {
|
||
color: #e6e6e6;
|
||
font-weight: bold;
|
||
font-size: 36rpx;
|
||
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* 好友列表容器 */
|
||
.friend-list-container {
|
||
flex: 1;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
/* 大卡片容器 */
|
||
.friend-list-card {
|
||
position: relative;
|
||
width: 100%;
|
||
flex: 1;
|
||
border-radius: 30rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||
z-index: 10;
|
||
}
|
||
|
||
.card-background {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
object-fit: cover;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.card-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.card-content {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 30rpx;
|
||
box-sizing: border-box;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.friend-count {
|
||
width: 100%;
|
||
padding: 20rpx 0;
|
||
text-align: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.count-text {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-weight: bold;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.scroll-view {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 0;
|
||
}
|
||
|
||
.friend-list {
|
||
width: 100%;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
/* 好友卡片样式 */
|
||
.friend-card {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 20rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.friend-avatar {
|
||
margin-right: 30rpx;
|
||
}
|
||
|
||
|
||
.friend-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.friend-nickname {
|
||
font-size: 36rpx;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
color: #333333;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.friend-uid {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.friend-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.action-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.action-btn:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.visit-btn {
|
||
background: transparent;
|
||
}
|
||
|
||
.action-icon {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: transparent;
|
||
}
|
||
|
||
|
||
.action-label {
|
||
font-size: 20rpx;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-more,
|
||
.no-more {
|
||
width: 100%;
|
||
padding: 40rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.loading-text,
|
||
.no-more-text {
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
/* 添加好友容器 */
|
||
.add-friend-container {
|
||
flex: 1;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.add-friend-card-content {
|
||
justify-content: flex-start;
|
||
align-items: flex-start;
|
||
padding-top: 30rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.search-wrapper {
|
||
width: 100%;
|
||
padding: 0;
|
||
margin-bottom: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border-radius: 20rpx;
|
||
padding: 0 30rpx;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.search-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
background: transparent;
|
||
border: none;
|
||
font-size: 32rpx;
|
||
color: #e6e6e6;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.search-input::placeholder {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.clear-btn {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
margin-left: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.clear-btn:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.clear-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
}
|
||
|
||
/* 好友请求容器 */
|
||
.friend-request-container {
|
||
flex: 1;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
}
|
||
|
||
.request-placeholder {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 100rpx 0;
|
||
}
|
||
|
||
.placeholder-text {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* 搜索结果区域 */
|
||
.search-result-area {
|
||
width: 100%;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.search-status {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 28rpx;
|
||
color: #ff6b9d;
|
||
}
|
||
|
||
.add-friend-btn {
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
color: #e6e6e6;
|
||
border: none;
|
||
border-radius: 50rpx;
|
||
padding: 0 32rpx;
|
||
height: 90rpx;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.add-friend-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.add-friend-btn:active {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 分隔线 */
|
||
.divider {
|
||
width: 100%;
|
||
height: 2rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
margin: 40rpx 0;
|
||
}
|
||
|
||
/* 已发送请求区域 */
|
||
.sent-requests-section {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.section-header {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.loading-status,
|
||
.empty-status {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
.request-list {
|
||
width: 100%;
|
||
}
|
||
|
||
.request-card {
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 20rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.request-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.request-message {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 10rpx 24rpx;
|
||
border-radius: 30rpx;
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.status-pending {
|
||
background: linear-gradient(135deg, #FFA726 0%, #FB8C00 100%);
|
||
color: #e6e6e6;
|
||
}
|
||
|
||
.status-accepted {
|
||
background: linear-gradient(135deg, #66BB6A 0%, #43A047 100%);
|
||
color: #e6e6e6;
|
||
}
|
||
|
||
.status-rejected {
|
||
background: linear-gradient(135deg, #EF5350 0%, #E53935 100%);
|
||
color: #e6e6e6;
|
||
}
|
||
|
||
.status-cancelled {
|
||
background: linear-gradient(135deg, #BDBDBD 0%, #9E9E9E 100%);
|
||
color: #e6e6e6;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
/* 收到的好友请求操作按钮 */
|
||
.request-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.request-actions .action-btn {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.request-actions .action-btn:active {
|
||
transform: scale(0.9);
|
||
}
|
||
|
||
.request-actions .accept-btn {
|
||
background: transparent;
|
||
}
|
||
|
||
.request-actions .reject-btn {
|
||
background: transparent;
|
||
}
|
||
|
||
.request-actions .action-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
}
|
||
|
||
/* 自定义添加好友弹窗 */
|
||
.custom-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 580rpx;
|
||
background: #e6e6e6;
|
||
border-radius: 30rpx;
|
||
padding: 50rpx 40rpx 40rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 38rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.modal-message {
|
||
font-size: 30rpx;
|
||
color: #666666;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.modal-buttons {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.modal-btn {
|
||
flex: 1;
|
||
height: 90rpx;
|
||
border-radius: 50rpx;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn-cancel {
|
||
background: #f5f5f5;
|
||
color: #666666;
|
||
}
|
||
|
||
.modal-btn-confirm {
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
color: #e6e6e6;
|
||
}
|
||
|
||
.modal-btn:active {
|
||
opacity: 0.8;
|
||
}
|
||
</style>
|