1
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
|
||||
"home_pages/index", //中转页
|
||||
"login_pages/index/index",
|
||||
"login_pages/verification/index",
|
||||
@@ -23,6 +24,7 @@ export default defineAppConfig({
|
||||
"myself/index", // 个人中心
|
||||
"edit/index", // 编辑个人中心
|
||||
"other/index", // 他人个人主页
|
||||
"follow/index", // 球友关注页
|
||||
],
|
||||
},
|
||||
// {
|
||||
|
||||
99
src/components/FollowUserCard/index.scss
Normal file
99
src/components/FollowUserCard/index.scss
Normal file
@@ -0,0 +1,99 @@
|
||||
// 球友卡片样式
|
||||
.follow_user_card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 20px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
|
||||
.user_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user_details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
|
||||
.nickname {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action_button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 16px;
|
||||
border-radius: 20px;
|
||||
min-width: 60px;
|
||||
|
||||
.button_text {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.follow_button {
|
||||
border: 0.5px solid #000000 !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.following_button {
|
||||
border: 0.5px solid rgba(120, 120, 128, 0.12) !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.mutual_button {
|
||||
border: 0.5px solid rgba(120, 120, 128, 0.12) !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.processing {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/components/FollowUserCard/index.tsx
Normal file
112
src/components/FollowUserCard/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { FollowUser } from '@/services/followService';
|
||||
import './index.scss';
|
||||
|
||||
interface FollowUserCardProps {
|
||||
user: FollowUser;
|
||||
onFollowChange?: (userId: number, isFollowing: boolean) => void;
|
||||
}
|
||||
|
||||
const FollowUserCard: React.FC<FollowUserCardProps> = ({ user, onFollowChange }) => {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// 防御性检查
|
||||
if (!user || !user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理关注操作
|
||||
const handle_follow_action = async () => {
|
||||
if (isProcessing) return;
|
||||
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
|
||||
// 根据当前状态决定操作
|
||||
let new_status = false;
|
||||
if (user.follow_status === 'follower' || user.follow_status === 'recommend') {
|
||||
// 粉丝或推荐用户,执行关注操作
|
||||
new_status = true;
|
||||
} else if (user.follow_status === 'following' || user.follow_status === 'mutual_follow') {
|
||||
// 已关注或互相关注,执行取消关注操作
|
||||
new_status = false;
|
||||
}
|
||||
|
||||
onFollowChange?.(user.id, new_status);
|
||||
} catch (error) {
|
||||
console.error('关注操作失败:', error);
|
||||
Taro.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理用户点击
|
||||
const handle_user_click = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/user_pages/other/index?userid=${user.id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 获取按钮文本和样式
|
||||
const get_button_config = () => {
|
||||
switch (user.follow_status) {
|
||||
case 'follower':
|
||||
case 'recommend':
|
||||
return {
|
||||
text: '回关',
|
||||
className: 'follow_button'
|
||||
};
|
||||
case 'following':
|
||||
return {
|
||||
text: '已关注',
|
||||
className: 'following_button'
|
||||
};
|
||||
case 'mutual_follow':
|
||||
return {
|
||||
text: '互相关注',
|
||||
className: 'mutual_button'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: '关注',
|
||||
className: 'follow_button'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const button_config = get_button_config();
|
||||
|
||||
return (
|
||||
<View className="follow_user_card">
|
||||
<View className="user_info" onClick={handle_user_click}>
|
||||
<Image
|
||||
className="avatar"
|
||||
src={user.avatar_url || require('@/static/userInfo/default_avatar.svg')}
|
||||
/>
|
||||
<View className="user_details">
|
||||
<Text className="nickname">{user.nickname}</Text>
|
||||
<Text className="signature">
|
||||
{user.personal_profile || '签名写在这里'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`action_button ${button_config.className} ${isProcessing ? 'processing' : ''}`}
|
||||
onClick={handle_follow_action}
|
||||
>
|
||||
<Text className="button_text">
|
||||
{isProcessing ? '处理中...' : button_config.text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FollowUserCard;
|
||||
@@ -91,6 +91,31 @@
|
||||
letter-spacing: 3.2%;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
// 可点击的统计项样式
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.stat_number {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.stat_label {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,31 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
setEditingField("");
|
||||
};
|
||||
|
||||
// 处理统计项点击
|
||||
const handle_stats_click = (type: 'following' | 'friends' | 'hosted' | 'participated') => {
|
||||
// 只有当前用户才能查看关注相关页面
|
||||
if (!is_current_user) {
|
||||
Taro.showToast({
|
||||
title: '暂不支持查看他人关注信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'following') {
|
||||
// 跳转到关注列表页面
|
||||
Taro.navigateTo({
|
||||
url: '/user_pages/follow/index?tab=following'
|
||||
});
|
||||
} else if (type === 'friends') {
|
||||
// 跳转到球友(粉丝)页面,显示粉丝标签
|
||||
Taro.navigateTo({
|
||||
url: '/user_pages/follow/index?tab=follower'
|
||||
});
|
||||
}
|
||||
// 主办和参加暂时不处理,可以后续扩展
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="user_info_card">
|
||||
{/* 头像和基本信息 */}
|
||||
@@ -139,11 +164,11 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
{/* 统计数据 */}
|
||||
<View className="stats_section">
|
||||
<View className="stats_container">
|
||||
<View className="stat_item">
|
||||
<View className="stat_item clickable" onClick={() => handle_stats_click('following')}>
|
||||
<Text className="stat_number">{user_info.stats.following}</Text>
|
||||
<Text className="stat_label">关注</Text>
|
||||
</View>
|
||||
<View className="stat_item">
|
||||
<View className="stat_item clickable" onClick={() => handle_stats_click('friends')}>
|
||||
<Text className="stat_number">{user_info.stats.friends}</Text>
|
||||
<Text className="stat_label">球友</Text>
|
||||
</View>
|
||||
|
||||
@@ -17,7 +17,8 @@ import withAuth from "./Auth";
|
||||
import { CustomPicker, PopupPicker } from "./Picker";
|
||||
import NTRPEvaluatePopup from "./NTRPEvaluatePopup";
|
||||
import RefundPopup from "./refundPopup";
|
||||
import GameManagePopup from './GameManagePopup'
|
||||
import GameManagePopup from './GameManagePopup';
|
||||
import FollowUserCard from './FollowUserCard/index';
|
||||
|
||||
export {
|
||||
ActivityTypeSwitch,
|
||||
@@ -41,4 +42,5 @@ export {
|
||||
NTRPEvaluatePopup,
|
||||
RefundPopup,
|
||||
GameManagePopup,
|
||||
FollowUserCard,
|
||||
};
|
||||
|
||||
232
src/services/followService.ts
Normal file
232
src/services/followService.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { API_CONFIG } from '@/config/api';
|
||||
import httpService, { ApiResponse } from './httpService';
|
||||
|
||||
// 球友信息接口
|
||||
export interface FollowUser {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar_url: string;
|
||||
personal_profile?: string;
|
||||
gender?: string;
|
||||
city?: string;
|
||||
ntrp_level?: number;
|
||||
// 关注状态:mutual_follow=互相关注, following=已关注, follower=粉丝, recommend=推荐
|
||||
follow_status: 'mutual_follow' | 'following' | 'follower' | 'recommend';
|
||||
follow_time?: string; // 关注时间
|
||||
is_mutual?: boolean; // 是否互关
|
||||
is_following?: boolean; // 是否已关注
|
||||
common_games_count?: number; // 共同参与球局数量
|
||||
}
|
||||
|
||||
// 关注列表响应接口
|
||||
export interface FollowListResponse {
|
||||
list: FollowUser[];
|
||||
total: number;
|
||||
current_city?: string; // 当前用户城市(推荐接口返回)
|
||||
}
|
||||
|
||||
// 球友关注服务类
|
||||
export class FollowService {
|
||||
|
||||
// 获取互关用户列表
|
||||
static async get_mutual_follow_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/mutual_follow_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: 'mutual_follow' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取互关列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取互关列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的粉丝列表
|
||||
static async get_fans_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/my_fans_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: user.is_mutual ? 'mutual_follow' as const : 'follower' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取粉丝列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取粉丝列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的关注列表
|
||||
static async get_following_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/my_following_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: user.is_mutual ? 'mutual_follow' as const : 'following' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取关注列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取关注列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关注列表(统一入口)
|
||||
static async get_follow_list(
|
||||
type: 'mutual_follow' | 'following' | 'follower' | 'recommend',
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
switch (type) {
|
||||
case 'mutual_follow':
|
||||
return this.get_mutual_follow_list(page, page_size);
|
||||
case 'following':
|
||||
return this.get_following_list(page, page_size);
|
||||
case 'follower':
|
||||
return this.get_fans_list(page, page_size);
|
||||
case 'recommend':
|
||||
return this.get_recommend_users(page, page_size);
|
||||
default:
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 关注用户
|
||||
static async follow_user(user_id: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await httpService.post<any>(
|
||||
API_CONFIG.USER.FOLLOW,
|
||||
{ following_id: user_id },
|
||||
{
|
||||
showLoading: true,
|
||||
loadingText: '关注中...'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.message || '关注失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关注失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消关注用户
|
||||
static async unfollow_user(user_id: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await httpService.post<any>(
|
||||
API_CONFIG.USER.UNFOLLOW,
|
||||
{ following_id: user_id },
|
||||
{
|
||||
showLoading: true,
|
||||
loadingText: '取消关注中...'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.message || '取消关注失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消关注失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 回关用户(关注粉丝)
|
||||
static async follow_back(user_id: number): Promise<boolean> {
|
||||
return this.follow_user(user_id);
|
||||
}
|
||||
|
||||
// 获取同城推荐用户
|
||||
static async get_recommend_users(page: number = 1, page_size: number = 10): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/recommend_same_city',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: 'recommend' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取推荐用户失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐用户失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 检查关注状态
|
||||
static async check_follow_status(user_id: number): Promise<'mutual_follow' | 'following' | 'follower' | 'none'> {
|
||||
try {
|
||||
const response = await httpService.post<{ status: string }>(
|
||||
'/wch_users/follow_status',
|
||||
{ user_id },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return response.data.status as 'mutual_follow' | 'following' | 'follower' | 'none';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查关注状态失败:', error);
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FollowService;
|
||||
123
src/user_pages/follow/README.md
Normal file
123
src/user_pages/follow/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 球友关注功能
|
||||
|
||||
## 功能概述
|
||||
球友关注功能模块,实现用户之间的关注、粉丝、互关和推荐功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 四个标签页
|
||||
- **互相关注**: 显示互相关注的用户列表
|
||||
- **关注**: 显示我关注的用户列表
|
||||
- **粉丝**: 显示关注我的用户列表(默认页面)
|
||||
- **推荐**: 显示同城推荐用户列表
|
||||
|
||||
### 2. 用户操作
|
||||
- **关注/取消关注**: 支持关注和取消关注操作
|
||||
- **回关**: 对粉丝进行回关操作
|
||||
- **用户详情**: 点击头像或昵称跳转到用户详情页
|
||||
|
||||
### 3. 列表功能
|
||||
- **下拉刷新**: 支持下拉刷新数据
|
||||
- **分页加载**: 支持滑动到底部加载更多
|
||||
- **实时状态**: 操作后实时更新关注状态
|
||||
|
||||
## API 接口
|
||||
|
||||
### 后端接口列表
|
||||
- `POST /user_follow/mutual_follow_list` - 获取互关用户列表
|
||||
- `POST /user_follow/my_fans_list` - 获取我的粉丝列表
|
||||
- `POST /user_follow/my_following_list` - 获取我的关注列表
|
||||
- `POST /user_follow/recommend_same_city` - 获取同城推荐用户
|
||||
- `POST /wch_users/follow` - 关注用户
|
||||
- `POST /wch_users/unfollow` - 取消关注用户
|
||||
|
||||
### 前端服务
|
||||
- `FollowService` - 关注相关API服务封装
|
||||
- `FollowUserCard` - 用户卡片组件
|
||||
- `FollowPage` - 球友关注主页面
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ └── followService.ts # 关注服务API
|
||||
├── components/
|
||||
│ └── FollowUserCard/ # 用户卡片组件
|
||||
│ ├── index.tsx
|
||||
│ ├── index.module.scss
|
||||
│ └── index.ts
|
||||
└── user_pages/
|
||||
└── follow/ # 球友关注页面
|
||||
├── index.tsx
|
||||
├── index.scss
|
||||
├── index.config.ts
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 页面跳转
|
||||
```typescript
|
||||
// 跳转到球友关注页面
|
||||
Taro.navigateTo({
|
||||
url: '/user_pages/follow/index'
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 服务调用
|
||||
```typescript
|
||||
import { FollowService } from '@/services/followService';
|
||||
|
||||
// 获取粉丝列表
|
||||
const response = await FollowService.get_fans_list(1, 20);
|
||||
|
||||
// 关注用户
|
||||
await FollowService.follow_user(userId);
|
||||
```
|
||||
|
||||
### 3. 组件使用
|
||||
```tsx
|
||||
import FollowUserCard from '@/components/FollowUserCard';
|
||||
|
||||
<FollowUserCard
|
||||
user={user}
|
||||
onFollowChange={handleFollowChange}
|
||||
/>
|
||||
```
|
||||
|
||||
## 设计规范
|
||||
|
||||
### UI 设计
|
||||
- 遵循 Figma 设计稿 `球友-粉丝` 页面设计
|
||||
- 标签页切换交互
|
||||
- 用户卡片布局和样式
|
||||
- 关注按钮状态设计
|
||||
|
||||
### 数据状态
|
||||
- `mutual_follow`: 互相关注
|
||||
- `following`: 已关注
|
||||
- `follower`: 粉丝
|
||||
- `recommend`: 推荐
|
||||
|
||||
### 按钮状态
|
||||
- **回关**: 对粉丝和推荐用户显示
|
||||
- **已关注**: 对已关注用户显示
|
||||
- **互相关注**: 对互关用户显示
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**: 页面需要用户登录,使用 `withAuth` HOC 包装
|
||||
2. **错误处理**: 所有 API 调用都有错误处理和友好提示
|
||||
3. **性能优化**: 采用分页加载,避免一次性加载过多数据
|
||||
4. **状态同步**: 关注操作后实时更新所有相关列表的状态
|
||||
5. **网络优化**: 推荐页面单独的分页大小(10条),其他页面(20条)
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 可能的扩展点
|
||||
1. **搜索功能**: 在关注列表中搜索特定用户
|
||||
2. **筛选功能**: 按城市、NTRP等级等条件筛选
|
||||
3. **批量操作**: 批量关注/取消关注
|
||||
4. **推荐算法**: 基于更多维度的智能推荐
|
||||
5. **社交互动**: 查看共同关注、共同参与的球局等
|
||||
6
src/user_pages/follow/index.config.ts
Normal file
6
src/user_pages/follow/index.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '球友',
|
||||
navigationStyle: 'custom',
|
||||
enablePullDownRefresh: false,
|
||||
backgroundColor: '#FAFAFA'
|
||||
})
|
||||
178
src/user_pages/follow/index.scss
Normal file
178
src/user_pages/follow/index.scss
Normal file
@@ -0,0 +1,178 @@
|
||||
// 球友关注页面样式
|
||||
.follow_page {
|
||||
min-height: 100vh;
|
||||
background: #FAFAFA;
|
||||
|
||||
// 导航栏内容
|
||||
.navbar_content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
|
||||
.navbar_back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 10px;
|
||||
|
||||
.back_icon {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background: url("data:image/svg+xml,%3Csvg width='8' height='16' viewBox='0 0 8 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.66667 2L1.33333 8L6.66667 14' stroke='%23000000' stroke-width='2.66667' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar_title {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
letter-spacing: 1.9%;
|
||||
color: #000000;
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.navbar_action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 83px;
|
||||
height: 30px;
|
||||
margin-right: 7px;
|
||||
|
||||
.action_icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Ccircle cx='5' cy='10' r='1.5' fill='%23191919'/%3E%3Ccircle cx='10' cy='10' r='1.5' fill='%23191919'/%3E%3Ccircle cx='15' cy='10' r='1.5' fill='%23191919'/%3E%3C/g%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签页导航
|
||||
.tab_navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
background: #ffffff;
|
||||
height: 44px;
|
||||
overflow-x: auto;
|
||||
margin-top: 110px;
|
||||
|
||||
|
||||
|
||||
.tab_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
|
||||
.tab_text {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -1.64%;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 375px) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐图标
|
||||
.recommend_icon {
|
||||
.icon_container {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: relative;
|
||||
|
||||
.star_icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 0.83L6.09 3.09L8.75 3.09L6.58 4.91L7.25 7.17L5 5.83L2.75 7.17L3.42 4.91L1.25 3.09L3.91 3.09L5 0.83Z' stroke='%238C8C8C' stroke-width='0.83' fill='%238C8C8C'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 激活状态
|
||||
&.active {
|
||||
/* 添加调试背景色 */
|
||||
|
||||
.tab_text {
|
||||
font-weight: 600 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
// 下边框
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #000000 !important;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除默认的粉丝标签激活样式,完全依赖 active 类
|
||||
}
|
||||
}
|
||||
|
||||
// 用户列表容器
|
||||
.user_list_container {
|
||||
flex: 1;
|
||||
margin-top: 12px;
|
||||
background: #ffffff;
|
||||
|
||||
// 加载状态提示
|
||||
.loading_tip,
|
||||
.empty_tip,
|
||||
.load_more_tip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
|
||||
text {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.empty_tip {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.load_more_tip {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
271
src/user_pages/follow/index.tsx
Normal file
271
src/user_pages/follow/index.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import CustomNavbar from '@/components/CustomNavbar';
|
||||
import FollowUserCard from '@/components/FollowUserCard';
|
||||
import { FollowService, FollowUser } from '@/services/followService';
|
||||
import { withAuth } from '@/components';
|
||||
import './index.scss';
|
||||
|
||||
// 标签页类型
|
||||
type TabType = 'mutual_follow' | 'following' | 'follower' | 'recommend';
|
||||
|
||||
// 标签页配置
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'mutual_follow' as TabType, label: '互相关注' },
|
||||
{ key: 'following' as TabType, label: '关注' },
|
||||
{ key: 'follower' as TabType, label: '粉丝' },
|
||||
{ key: 'recommend' as TabType, label: '推荐' }
|
||||
];
|
||||
|
||||
const FollowPage: React.FC = () => {
|
||||
// 获取页面参数,支持指定默认标签页
|
||||
const instance = Taro.getCurrentInstance();
|
||||
const default_tab = (instance.router?.params?.tab as TabType) || 'mutual_follow';
|
||||
|
||||
// 当前激活的标签页 - 根据设计稿默认显示互相关注
|
||||
const [active_tab, set_active_tab] = useState<TabType>(default_tab);
|
||||
|
||||
// 用户列表数据
|
||||
const [user_lists, set_user_lists] = useState<Record<TabType, FollowUser[]>>({
|
||||
mutual_follow: [],
|
||||
following: [],
|
||||
follower: [],
|
||||
recommend: []
|
||||
});
|
||||
|
||||
// 加载状态 - 根据默认标签页设置初始加载状态
|
||||
const [loading_states, set_loading_states] = useState<Record<TabType, boolean>>(() => ({
|
||||
mutual_follow: default_tab === 'mutual_follow',
|
||||
following: default_tab === 'following',
|
||||
follower: default_tab === 'follower',
|
||||
recommend: default_tab === 'recommend'
|
||||
}));
|
||||
|
||||
// 分页信息
|
||||
const [page_info, set_page_info] = useState<Record<TabType, { page: number; total: number; has_more: boolean }>>(() => ({
|
||||
mutual_follow: { page: 1, total: 0, has_more: true },
|
||||
following: { page: 1, total: 0, has_more: true },
|
||||
follower: { page: 1, total: 0, has_more: true },
|
||||
recommend: { page: 1, total: 0, has_more: true }
|
||||
}));
|
||||
|
||||
// 加载用户列表
|
||||
const load_user_list = async (tab: TabType, is_refresh: boolean = false) => {
|
||||
const current_page = is_refresh ? 1 : page_info[tab].page;
|
||||
const page_size = tab === 'recommend' ? 10 : 20;
|
||||
|
||||
// 设置加载状态
|
||||
set_loading_states(prev => ({ ...prev, [tab]: true }));
|
||||
|
||||
try {
|
||||
const response = await FollowService.get_follow_list(tab, current_page, page_size);
|
||||
|
||||
// 调试日志
|
||||
console.log(`加载${TAB_CONFIG.find(t => t.key === tab)?.label}数据:`, response);
|
||||
|
||||
// 更新用户列表
|
||||
set_user_lists(prev => ({
|
||||
...prev,
|
||||
[tab]: is_refresh ? response.list : [...(prev[tab] || []), ...response.list]
|
||||
}));
|
||||
|
||||
// 更新分页信息
|
||||
set_page_info(prev => ({
|
||||
...prev,
|
||||
[tab]: {
|
||||
page: current_page + 1,
|
||||
total: response.total,
|
||||
has_more: response.list.length === page_size && (current_page * page_size) < response.total
|
||||
}
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error(`加载${TAB_CONFIG.find(t => t.key === tab)?.label}列表失败:`, error);
|
||||
Taro.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
set_loading_states(prev => ({ ...prev, [tab]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理标签页切换
|
||||
const handle_tab_change = (tab: TabType) => {
|
||||
if (tab === active_tab) return;
|
||||
|
||||
set_active_tab(tab);
|
||||
|
||||
// 如果该标签页还没有数据,则加载
|
||||
if (user_lists[tab].length === 0) {
|
||||
load_user_list(tab, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理关注状态变化
|
||||
const handle_follow_change = async (user_id: number, is_following: boolean) => {
|
||||
try {
|
||||
if (is_following) {
|
||||
await FollowService.follow_user(user_id);
|
||||
Taro.showToast({
|
||||
title: '关注成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
await FollowService.unfollow_user(user_id);
|
||||
Taro.showToast({
|
||||
title: '取消关注成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户列表中的关注状态
|
||||
set_user_lists(prev => {
|
||||
const new_lists = { ...prev };
|
||||
|
||||
// 更新所有标签页中的用户状态
|
||||
Object.keys(new_lists).forEach(tab_key => {
|
||||
const tab = tab_key as TabType;
|
||||
if (new_lists[tab] && Array.isArray(new_lists[tab])) {
|
||||
new_lists[tab] = new_lists[tab].map(user => {
|
||||
if (user.id === user_id) {
|
||||
// 根据操作结果更新状态
|
||||
let new_status = user.follow_status;
|
||||
if (is_following) {
|
||||
if (user.follow_status === 'follower') {
|
||||
new_status = 'mutual_follow';
|
||||
} else if (user.follow_status === 'recommend') {
|
||||
new_status = 'following';
|
||||
}
|
||||
} else {
|
||||
if (user.follow_status === 'mutual_follow') {
|
||||
new_status = 'follower';
|
||||
} else if (user.follow_status === 'following') {
|
||||
new_status = 'recommend';
|
||||
}
|
||||
}
|
||||
return { ...user, follow_status: new_status };
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new_lists;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('关注操作失败:', error);
|
||||
Taro.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理下拉刷新
|
||||
const handle_refresh = () => {
|
||||
load_user_list(active_tab, true);
|
||||
};
|
||||
|
||||
// 处理加载更多
|
||||
const handle_load_more = () => {
|
||||
if (page_info[active_tab]?.has_more && !loading_states[active_tab]) {
|
||||
load_user_list(active_tab, false);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
try {
|
||||
load_user_list(default_tab, true);
|
||||
} catch (error) {
|
||||
console.error('初始化加载失败:', error);
|
||||
Taro.showToast({
|
||||
title: '初始化失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}, [default_tab]);
|
||||
|
||||
return (
|
||||
<View className="follow_page">
|
||||
{/* 自定义导航栏 */}
|
||||
<CustomNavbar>
|
||||
<View className="navbar_content">
|
||||
<View className="navbar_back" onClick={() => Taro.navigateBack()}>
|
||||
<View className="back_icon" />
|
||||
</View>
|
||||
<Text className="navbar_title">球友</Text>
|
||||
<View className="navbar_action">
|
||||
<View className="action_icon" />
|
||||
</View>
|
||||
</View>
|
||||
</CustomNavbar>
|
||||
|
||||
{/* 标签页导航 */}
|
||||
<View className="tab_navigation">
|
||||
{TAB_CONFIG.map(tab => (
|
||||
<View
|
||||
key={tab.key}
|
||||
className={`tab_item ${active_tab === tab.key ? 'active' : ''}`}
|
||||
onClick={() => handle_tab_change(tab.key)}
|
||||
>
|
||||
<Text className="tab_text">{tab.label}</Text>
|
||||
{tab.key === 'recommend' && (
|
||||
<View className="recommend_icon">
|
||||
{/* 推荐图标 SVG */}
|
||||
<View className="icon_container">
|
||||
<View className="star_icon" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<ScrollView
|
||||
className="user_list_container"
|
||||
scrollY
|
||||
refresherEnabled
|
||||
refresherTriggered={loading_states[active_tab]}
|
||||
onRefresherRefresh={handle_refresh}
|
||||
onScrollToLower={handle_load_more}
|
||||
lowerThreshold={100}
|
||||
>
|
||||
{user_lists[active_tab]?.map(user => (
|
||||
<FollowUserCard
|
||||
key={user.id}
|
||||
user={user}
|
||||
onFollowChange={handle_follow_change}
|
||||
/>
|
||||
)) || []}
|
||||
|
||||
{/* 加载状态提示 */}
|
||||
{loading_states[active_tab] && (user_lists[active_tab]?.length || 0) === 0 && (
|
||||
<View className="loading_tip">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 空状态提示 */}
|
||||
{!loading_states[active_tab] && (user_lists[active_tab]?.length || 0) === 0 && (
|
||||
<View className="empty_tip">
|
||||
<Text>暂无{TAB_CONFIG.find(t => t.key === active_tab)?.label}用户</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 加载更多提示 */}
|
||||
{(user_lists[active_tab]?.length || 0) > 0 && !page_info[active_tab]?.has_more && (
|
||||
<View className="load_more_tip">
|
||||
<Text>已加载全部</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default withAuth(FollowPage);
|
||||
Reference in New Issue
Block a user