This commit is contained in:
张成
2025-09-17 22:55:20 +08:00
parent 46fc702dfd
commit 721d0c10e4
11 changed files with 1078 additions and 3 deletions

View 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. **社交互动**: 查看共同关注、共同参与的球局等

View File

@@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '球友',
navigationStyle: 'custom',
enablePullDownRefresh: false,
backgroundColor: '#FAFAFA'
})

View 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;
}
}
}

View 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);