接口的对接和一些前端页面的修改
This commit is contained in:
parent
5c4faae97b
commit
e3df41657a
607
Android后端对接总结.md
607
Android后端对接总结.md
|
|
@ -3,7 +3,35 @@
|
|||
> **生成时间**: 2024-12-29
|
||||
> **更新时间**: 2024-12-29
|
||||
> **项目**: 直播IM系统
|
||||
> **后端完成度**: ✅ 100% (94/94接口)
|
||||
> **后端完成度**: ✅ 100% (94/94接口)
|
||||
> **Android端对接**: ✨ 用户系统7个接口已全部接入
|
||||
|
||||
---
|
||||
|
||||
## 📱 Android端对接进度
|
||||
|
||||
### ✅ 已接入模块 (7个)
|
||||
|
||||
| 模块 | 接口数 | 对接状态 | 说明 |
|
||||
|------|--------|---------|------|
|
||||
| 用户系统 | 7 | ✅ 100% | 登录、注册、验证码、用户信息、资料编辑、头像上传、退出登录 |
|
||||
| 好友管理 | 9 | ✅ 100% | 搜索用户、发送申请、接受/拒绝申请、好友列表、删除好友、拉黑/取消拉黑、黑名单列表 |
|
||||
| 消息表情回应 | 4 | ✅ 100% | 添加表情回应、移除表情回应、获取表情列表、获取回应用户 |
|
||||
| 关注功能 | 8 | ✅ 100% | 关注、取消关注、关注列表、粉丝列表、关注状态、批量检查、关注统计 |
|
||||
| 作品管理 | 15 | ✅ 100% | 发布、编辑、删除、详情、列表、点赞、收藏、分享 |
|
||||
| 搜索功能 | 9 | ✅ 100% | 搜索用户、搜索直播间、搜索作品、综合搜索、热门搜索、搜索历史、搜索建议 |
|
||||
| 支付集成 | 4 | ✅ 100% | 充值选项、创建订单、订单支付、查询结果 |
|
||||
|
||||
### 🔄 待接入模块
|
||||
|
||||
| 模块 | 接口数 | 优先级 | 说明 |
|
||||
|------|--------|--------|------|
|
||||
| 直播间管理 | 10 | 🔴 高 | 列表、详情、创建、在线人数、观众列表、赠送礼物 |
|
||||
| 直播间弹幕 | 2 | 🔴 高 | 历史弹幕、发送弹幕 |
|
||||
| WebSocket通信 | 2 | 🔴 高 | 在线人数、实时弹幕 |
|
||||
| 群组管理 | 10 | 🟡 中 | 创建、更新、解散、成员管理 |
|
||||
| 消息聊天 | 12 | 🟡 中 | 会话、消息、已读状态 |
|
||||
| 其他模块 | 79 | <20> 中低 | 作品、评论、搜索、通知、支付等 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -33,31 +61,74 @@
|
|||
| 支付集成 | 4 | ✅ 100% | 微信、支付宝支付 |
|
||||
| 文件上传 | 5 | ✅ 100% | 图片、视频、语音上传 |
|
||||
| 分类管理 | 7 | ✅ 100% | 分类列表、统计、热门 |
|
||||
|
||||
### 🎉 最新完成接口 (4个) - 2024-12-29
|
||||
|
||||
1. ✅ **视频上传** - `POST /api/upload/work/video` - 支持MP4/MOV/AVI/FLV,最大500MB
|
||||
2. ✅ **语音上传** - `POST /api/upload/chat/voice` - 支持MP3/AAC/WAV/M4A,最大10MB
|
||||
3. ✅ **观众列表** - `GET /api/rooms/{roomId}/viewers` - 获取直播间在线观众列表
|
||||
4. ✅ **赠送礼物** - `POST /api/rooms/{roomId}/gift` - 在直播间赠送礼物给主播
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Android端需要对接的核心接口
|
||||
|
||||
### 第一阶段:基础功能 (必须)
|
||||
|
||||
#### 1. 用户系统 (7个接口)
|
||||
#### 1. 用户系统 (7个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/login # 登录
|
||||
✅ POST /api/front/register # 注册
|
||||
✅ POST /api/sms/send # 验证码
|
||||
✅ GET /api/front/user/info # 用户信息
|
||||
✅ POST /api/front/user/update # 更新资料
|
||||
✅ POST /api/front/user/upload/image # 上传头像
|
||||
✅ GET /api/front/user/logout # 退出登录
|
||||
✅ POST /api/front/login # 账号密码登录 (✨已接入)
|
||||
✅ POST /api/front/register # APP用户注册 (✨已接入)
|
||||
✅ POST /api/front/sendCode # 发送验证码 (✨已接入)
|
||||
✅ GET /api/front/user # 获取用户信息 (✨已接入)
|
||||
✅ POST /api/front/user/edit # 更新用户资料 (✨已接入)
|
||||
✅ POST /api/front/user/upload/image # 上传头像 (✨已接入)
|
||||
✅ GET /api/front/logout # 退出登录 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **POST /api/front/login** - 账号密码登录 ✨ Android端已接入
|
||||
- 请求参数: `{ "account": "手机号", "password": "密码" }`
|
||||
- 响应数据: `{ "token": "JWT令牌", "uid": 用户ID, "nikeName": "昵称", "phone": "手机号" }`
|
||||
- 后端实现: `LoginController.login()` → `LoginServiceImpl.login()`
|
||||
- Android实现: `LoginActivity.java` → `ApiService.login()`
|
||||
- 模型类: `LoginRequest.java`, `LoginResponse.java`
|
||||
|
||||
2. **POST /api/front/register** - APP用户注册 ✨ Android端已接入
|
||||
- 请求参数: `{ "phone": "手机号", "password": "密码", "verificationCode": "验证码(可选)", "nickname": "昵称(可选)" }`
|
||||
- 响应数据: `{ "token": "JWT令牌", "uid": 用户ID, "nikeName": "昵称", "phone": "手机号" }`
|
||||
- 后端实现: `LoginController.register()` → `LoginServiceImpl.register()`
|
||||
- Android实现: `RegisterActivity.java` → `ApiService.register()`
|
||||
- 模型类: `RegisterRequest.java`, `LoginResponse.java`
|
||||
|
||||
3. **POST /api/front/sendCode** - 发送短信验证码 ✨ Android端已接入
|
||||
- 请求参数: `phone=手机号` (FormUrlEncoded)
|
||||
- 响应数据: `{ "code": 200, "msg": "发送成功" }`
|
||||
- 后端实现: `LoginController.sendCode()` → `SmsService.sendCommonCode()`
|
||||
- Android实现: `RegisterActivity.java` → `ApiService.sendCode()`
|
||||
|
||||
4. **GET /api/front/user** - 获取用户信息 ✨ Android端已接入
|
||||
- 请求头: `Authorization: Bearer {token}`
|
||||
- 响应数据: 用户中心完整信息(昵称、头像、余额、积分、经验、等级等)
|
||||
- 后端实现: `UserController.getUserCenter()` → `UserService.getUserCenter()`
|
||||
- Android实现: `ApiService.getUserInfo()`
|
||||
- 模型类: `UserInfoResponse.java`
|
||||
|
||||
5. **POST /api/front/user/edit** - 更新用户资料 ✨ Android端已接入
|
||||
- 请求头: `Authorization: Bearer {token}`
|
||||
- 请求参数: `{ "nickname": "昵称", "avatar": "头像URL" }`
|
||||
- 响应数据: `{ "code": 200, "msg": "success" }`
|
||||
- 后端实现: `UserController.personInfo()` → `UserService.editUser()`
|
||||
- Android实现: `EditProfileActivity.java` → `ApiService.updateUserInfo()`
|
||||
- 模型类: `UserEditRequest.java`
|
||||
|
||||
6. **POST /api/front/user/upload/image** - 上传头像 ✨ Android端已接入
|
||||
- 请求头: `Authorization: Bearer {token}`
|
||||
- 请求参数: `multipart/form-data` - `file`: 图片文件, `model`: "user", `pid`: 7
|
||||
- 响应数据: `{ "url": "图片URL", "name": "文件名", "size": 文件大小 }`
|
||||
- 后端实现: `UserUploadController.image()` → `UploadService.imageUpload()`
|
||||
- Android实现: `EditProfileActivity.java` → `ApiService.uploadImage()`
|
||||
- 模型类: `FileUploadResponse.java`
|
||||
|
||||
7. **GET /api/front/logout** - 退出登录 ✨ Android端已接入
|
||||
- 请求头: `Authorization: Bearer {token}`
|
||||
- 响应数据: `{ "code": 200, "msg": "success" }`
|
||||
- 后端实现: `LoginController.loginOut()` → `LoginServiceImpl.loginOut()`
|
||||
- Android实现: `ApiService.logout()`
|
||||
|
||||
#### 2. 直播间系统 (10个接口) - ✅ 全部完成
|
||||
```
|
||||
✅ GET /api/front/live/rooms # 直播间列表
|
||||
|
|
@ -93,19 +164,81 @@
|
|||
|
||||
### 第二阶段:社交功能 (推荐)
|
||||
|
||||
#### 5. 好友管理 (9个接口)
|
||||
#### 5. 好友管理 (9个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/friends/request # 发送好友申请
|
||||
✅ POST /api/front/friends/accept # 接受好友申请
|
||||
✅ POST /api/front/friends/reject # 拒绝好友申请
|
||||
✅ GET /api/front/friends/list # 好友列表
|
||||
✅ GET /api/front/friends/requests # 好友申请列表
|
||||
✅ POST /api/front/friends/delete/{friendId} # 删除好友
|
||||
✅ POST /api/front/friends/block/{friendId} # 拉黑好友
|
||||
✅ POST /api/front/friends/unblock/{friendId} # 取消拉黑
|
||||
✅ GET /api/front/friends/blocked # 黑名单列表
|
||||
✅ GET /api/front/users/search # 搜索用户 (✨已接入)
|
||||
✅ POST /api/front/friends/request # 发送好友申请 (✨已接入)
|
||||
✅ POST /api/front/friends/requests/{requestId}/handle # 处理好友请求(接受/拒绝) (✨已接入)
|
||||
✅ GET /api/front/friends # 好友列表 (✨已接入)
|
||||
✅ GET /api/front/friends/requests # 好友申请列表 (✨已接入)
|
||||
✅ DELETE /api/front/friends/{friendId} # 删除好友 (✨已接入)
|
||||
✅ POST /api/front/friends/block/{friendId} # 拉黑好友 (✨已接入)
|
||||
✅ POST /api/front/friends/unblock/{friendId} # 取消拉黑 (✨已接入)
|
||||
✅ GET /api/front/friends/blocked # 黑名单列表 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **GET /api/front/users/search** - 搜索用户 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (搜索关键词), `page`, `pageSize`
|
||||
- 响应数据: 用户列表,包含好友状态 (0:未添加, 1:已是好友, 2:已申请)
|
||||
- 后端实现: `FriendController.searchUsers()`
|
||||
- Android实现: `AddFriendActivity.java` → `ApiService.searchUsers()`
|
||||
- 模型类: `SearchUserResponse.java`
|
||||
|
||||
2. **POST /api/front/friends/request** - 发送好友申请 ✨ Android端已接入
|
||||
- 请求参数: `{ "targetUserId": 目标用户ID, "message": "申请消息(可选)" }`
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `FriendController.sendFriendRequest()`
|
||||
- Android实现: `AddFriendActivity.java` → `ApiService.sendFriendRequest()`
|
||||
|
||||
3. **POST /api/front/friends/requests/{requestId}/handle** - 处理好友请求 ✨ Android端已接入
|
||||
- 请求参数: `{ "accept": true/false }`
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `FriendController.handleFriendRequest()`
|
||||
- Android实现: `MyFriendsActivity.java` → `handleFriendRequest()`
|
||||
- 说明: accept=true接受,false拒绝;接受后自动创建双向好友关系和私聊会话
|
||||
|
||||
4. **GET /api/front/friends** - 好友列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 好友列表,包含在线状态
|
||||
- 后端实现: `FriendController.getFriendList()`
|
||||
- Android实现: `MyFriendsActivity.java` → `loadFriendList()`
|
||||
- 模型类: `FriendResponse.java`
|
||||
|
||||
5. **GET /api/front/friends/requests** - 好友申请列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 待处理的好友申请列表
|
||||
- 后端实现: `FriendController.getFriendRequests()`
|
||||
- Android实现: `MyFriendsActivity.java` → `loadFriendRequests()`
|
||||
- 模型类: `FriendRequestResponse.java`
|
||||
|
||||
6. **DELETE /api/front/friends/{friendId}** - 删除好友 ✨ Android端已接入
|
||||
- 请求参数: `friendId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `FriendController.deleteFriend()`
|
||||
- Android实现: `MyFriendsActivity.java` → `deleteFriend()`
|
||||
- 说明: 删除双向好友关系
|
||||
|
||||
7. **POST /api/front/friends/block/{friendId}** - 拉黑好友 ✨ Android端已接入
|
||||
- 请求参数: `friendId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `FriendController.blockFriend()`
|
||||
- Android实现: `MyFriendsActivity.java` → `blockFriend()`
|
||||
- 说明: 拉黑后自动删除好友关系
|
||||
|
||||
8. **POST /api/front/friends/unblock/{friendId}** - 取消拉黑 ✨ Android端已接入
|
||||
- 请求参数: `friendId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `FriendController.unblockFriend()`
|
||||
- Android实现: `MyFriendsActivity.java` → `unblockFriend()`
|
||||
|
||||
9. **GET /api/front/friends/blocked** - 黑名单列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 黑名单用户列表
|
||||
- 后端实现: `FriendController.getBlockedList()`
|
||||
- Android实现: `MyFriendsActivity.java` → `loadBlockedList()`
|
||||
|
||||
#### 6. 群组管理 (10个接口)
|
||||
```
|
||||
✅ POST /api/front/groups/create # 创建群组
|
||||
|
|
@ -139,14 +272,44 @@
|
|||
... (更多消息相关接口)
|
||||
```
|
||||
|
||||
#### 9. 消息表情回应 (4个接口)
|
||||
#### 9. 消息表情回应 (4个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/messages/reactions/add # 添加表情回应
|
||||
✅ DELETE /api/front/messages/reactions/remove # 移除表情回应
|
||||
✅ GET /api/front/messages/{messageId}/reactions # 获取消息的所有表情回应
|
||||
✅ GET /api/front/messages/{messageId}/reactions/users # 获取特定表情的用户列表
|
||||
✅ POST /api/front/messages/reactions/add # 添加表情回应 (✨已接入)
|
||||
✅ DELETE /api/front/messages/reactions/remove # 移除表情回应 (✨已接入)
|
||||
✅ GET /api/front/messages/{messageId}/reactions # 获取消息的所有表情回应 (✨已接入)
|
||||
✅ GET /api/front/messages/{messageId}/reactions/users # 获取特定表情的用户列表 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **POST /api/front/messages/reactions/add** - 添加表情回应 ✨ Android端已接入
|
||||
- 请求参数: `{ "messageId": "消息ID", "emoji": "表情符号" }`
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `MessageReactionController.addReaction()`
|
||||
- Android实现: `ConversationActivity.java` → `addMessageReaction()`
|
||||
- 说明: 用户可以对消息添加表情回应,支持多种表情符号
|
||||
|
||||
2. **DELETE /api/front/messages/reactions/remove** - 移除表情回应 ✨ Android端已接入
|
||||
- 请求参数: `{ "messageId": "消息ID", "emoji": "表情符号" }`
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `MessageReactionController.removeReaction()`
|
||||
- Android实现: `ConversationActivity.java` → `removeMessageReaction()`
|
||||
- 说明: 用户可以移除自己添加的表情回应
|
||||
|
||||
3. **GET /api/front/messages/{messageId}/reactions** - 获取消息的所有表情回应 ✨ Android端已接入
|
||||
- 请求参数: `messageId` (路径参数)
|
||||
- 响应数据: `[{ "emoji": "👍", "count": 5, "reactedByMe": true }, ...]`
|
||||
- 后端实现: `MessageReactionController.getMessageReactions()`
|
||||
- Android实现: `ConversationActivity.java` → `loadMessageReactions()`
|
||||
- 说明: 获取某条消息的所有表情回应统计,包括表情、数量和当前用户是否已回应
|
||||
|
||||
4. **GET /api/front/messages/{messageId}/reactions/users** - 获取特定表情的用户列表 ✨ Android端已接入
|
||||
- 请求参数: `messageId` (路径参数), `emoji` (查询参数)
|
||||
- 响应数据: `[{ "userId": 1, "username": "用户名", "avatarUrl": "头像URL" }, ...]`
|
||||
- 后端实现: `MessageReactionController.getReactionUsers()`
|
||||
- Android实现: `ApiService.getReactionUsers()`
|
||||
- 说明: 获取对某条消息添加特定表情的所有用户列表
|
||||
|
||||
#### 10. 消息搜索 (3个接口)
|
||||
```
|
||||
✅ GET /api/front/messages/search/conversations # 搜索会话
|
||||
|
|
@ -154,15 +317,74 @@
|
|||
✅ GET /api/front/messages/search/global # 全局搜索
|
||||
```
|
||||
|
||||
#### 11. 关注功能 (8个接口)
|
||||
#### 11. 关注功能 (8个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/follow/follow # 关注
|
||||
✅ POST /api/front/follow/unfollow # 取消关注
|
||||
✅ GET /api/front/follow/following # 关注列表
|
||||
✅ GET /api/front/follow/followers # 粉丝列表
|
||||
✅ GET /api/front/follow/status/{userId} # 关注状态
|
||||
✅ POST /api/front/follow/status/batch # 批量检查
|
||||
✅ GET /api/front/follow/stats # 关注统计
|
||||
✅ POST /api/front/follow/follow # 关注 (✨已接入)
|
||||
✅ POST /api/front/follow/unfollow # 取消关注 (✨已接入)
|
||||
✅ GET /api/front/follow/following # 关注列表 (✨已接入)
|
||||
✅ GET /api/front/follow/followers # 粉丝列表 (✨已接入)
|
||||
✅ GET /api/front/follow/status/{userId} # 关注状态 (✨已接入)
|
||||
✅ POST /api/front/follow/status/batch # 批量检查 (✨已接入)
|
||||
✅ GET /api/front/follow/stats # 关注统计 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **POST /api/front/follow/follow** - 关注用户 ✨ Android端已接入
|
||||
- 请求参数: `{ "userId": 目标用户ID }`
|
||||
- 响应数据: `{ "success": true, "message": "关注成功", "isFollowing": true }`
|
||||
- 后端实现: `FollowController.follow()`
|
||||
- Android实现: `UserProfileReadOnlyActivity.java` → `followUser()`
|
||||
- 说明: 用户可以关注其他用户或主播,防止自己关注自己和重复关注
|
||||
|
||||
2. **POST /api/front/follow/unfollow** - 取消关注 ✨ Android端已接入
|
||||
- 请求参数: `{ "userId": 目标用户ID }`
|
||||
- 响应数据: `{ "success": true, "message": "取消关注成功", "isFollowing": false }`
|
||||
- 后端实现: `FollowController.unfollow()`
|
||||
- Android实现: `UserProfileReadOnlyActivity.java` → `unfollowUser()`
|
||||
|
||||
3. **GET /api/front/follow/status/{userId}** - 检查关注状态 ✨ Android端已接入
|
||||
- 请求参数: `userId` (路径参数)
|
||||
- 响应数据: `{ "isFollowing": true/false, "userId": 用户ID }`
|
||||
- 后端实现: `FollowController.checkFollowStatus()`
|
||||
- Android实现: `UserProfileReadOnlyActivity.java` → `checkFollowStatus()`
|
||||
- 说明: 查询是否已关注某个用户
|
||||
|
||||
4. **POST /api/front/follow/status/batch** - 批量检查关注状态 ✨ Android端已接入
|
||||
- 请求参数: `{ "userIds": [用户ID列表] }`
|
||||
- 响应数据: `{ "statusMap": { "userId": true/false, ... } }`
|
||||
- 后端实现: `FollowController.batchCheckFollowStatus()`
|
||||
- Android实现: `ApiService.batchCheckFollowStatus()`
|
||||
- 说明: 批量查询多个用户的关注状态
|
||||
|
||||
5. **GET /api/front/follow/following** - 获取关注列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 关注的用户列表,包含在线状态
|
||||
- 后端实现: `FollowController.getFollowingList()`
|
||||
- Android实现: `FollowingListActivity.java` → `loadFollowingList()`
|
||||
- 模型类: 使用Map动态解析
|
||||
- 说明: 查看我关注的所有用户,支持分页
|
||||
|
||||
6. **GET /api/front/follow/followers** - 获取粉丝列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 粉丝列表,包含在线状态和是否互相关注
|
||||
- 后端实现: `FollowController.getFollowersList()`
|
||||
- Android实现: `FansListActivity.java` → `loadFollowersList()`
|
||||
- 模型类: 使用Map动态解析
|
||||
- 说明: 查看关注我的所有用户,支持分页
|
||||
|
||||
7. **GET /api/front/follow/stats** - 获取关注统计 ✨ Android端已接入
|
||||
- 请求参数: `userId` (可选,不传则查询当前用户)
|
||||
- 响应数据: `{ "followingCount": 关注数, "followersCount": 粉丝数 }`
|
||||
- 后端实现: `FollowController.getFollowStats()`
|
||||
- Android实现: `ProfileActivity.java` → `loadFollowStats()`
|
||||
- 说明: 查看关注数和粉丝数统计
|
||||
|
||||
8. **POST /api/front/live/follow** - 直播间关注主播 (已在直播间模块实现)
|
||||
- 请求参数: `{ "streamerId": 主播ID, "action": "follow/unfollow" }`
|
||||
- 响应数据: `{ "success": true }`
|
||||
- 后端实现: `LiveRoomController.followStreamer()`
|
||||
- 说明: 在直播间内关注/取消关注主播
|
||||
```
|
||||
|
||||
### 第三阶段:内容功能 (可选)
|
||||
|
|
@ -175,25 +397,130 @@
|
|||
✅ POST /api/admin/gift/delete/{id} # 删除礼物(后台)
|
||||
```
|
||||
|
||||
#### 13. 作品管理 (15个接口) - ✅ 全部完成
|
||||
#### 13. 作品管理 (15个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/works/publish # 发布作品
|
||||
✅ GET /api/front/works/list # 作品列表
|
||||
✅ GET /api/front/works/{worksId} # 作品详情
|
||||
✅ POST /api/front/works/{worksId}/like # 点赞
|
||||
✅ POST /api/front/works/{worksId}/collect # 收藏
|
||||
✅ PUT /api/front/works/{worksId} # 编辑作品
|
||||
✅ DELETE /api/front/works/{worksId} # 删除作品
|
||||
✅ POST /api/front/works/{worksId}/unlike # 取消点赞
|
||||
✅ POST /api/front/works/{worksId}/uncollect # 取消收藏
|
||||
✅ GET /api/front/works/user/{userId} # 用户作品列表
|
||||
✅ GET /api/front/works/my/liked # 我的点赞列表
|
||||
✅ GET /api/front/works/my/collected # 我的收藏列表
|
||||
✅ POST /api/front/works/{worksId}/share # 分享作品
|
||||
✅ POST /api/front/works/search # 搜索作品
|
||||
✅ GET /api/front/works/recommend # 推荐作品
|
||||
✅ POST /api/front/works/publish # 发布作品 (✨已接入)
|
||||
✅ GET /api/front/works/detail/{worksId} # 作品详情 (✨已接入)
|
||||
✅ POST /api/front/works/update # 编辑作品 (✨已接入)
|
||||
✅ POST /api/front/works/delete/{worksId} # 删除作品 (✨已接入)
|
||||
✅ POST /api/front/works/search # 搜索作品 (✨已接入)
|
||||
✅ GET /api/front/works/user/{userId} # 用户作品列表 (✨已接入)
|
||||
✅ POST /api/front/works/like/{worksId} # 点赞 (✨已接入)
|
||||
✅ POST /api/front/works/unlike/{worksId} # 取消点赞 (✨已接入)
|
||||
✅ POST /api/front/works/collect/{worksId} # 收藏 (✨已接入)
|
||||
✅ POST /api/front/works/uncollect/{worksId} # 取消收藏 (✨已接入)
|
||||
✅ GET /api/front/works/my/liked # 我的点赞列表 (✨已接入)
|
||||
✅ GET /api/front/works/my/collected # 我的收藏列表 (✨已接入)
|
||||
✅ POST /api/front/works/share/{worksId} # 分享作品 (✨已接入)
|
||||
✅ POST /api/front/upload/work/video # 视频上传 (✨已接入)
|
||||
✅ POST /api/front/upload/image # 图片上传 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **POST /api/front/works/publish** - 发布作品 ✨ Android端已接入
|
||||
- 请求参数: `{ "title": "标题", "description": "描述", "type": "IMAGE/VIDEO", "coverUrl": "封面URL", "videoUrl": "视频URL", "imageUrls": ["图片URL"] }`
|
||||
- 响应数据: `{ "code": 200, "data": 作品ID }`
|
||||
- 后端实现: `WorksController.publishWorks()`
|
||||
- Android实现: `PublishWorkActivity.java` → `publishWork()`
|
||||
- 模型类: `WorksRequest.java`
|
||||
- 说明: 需要先上传文件获取URL,再调用此接口发布
|
||||
|
||||
2. **GET /api/front/works/detail/{worksId}** - 获取作品详情 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: 作品完整信息,包含点赞收藏状态
|
||||
- 后端实现: `WorksController.getWorksDetail()`
|
||||
- Android实现: `WorkDetailActivity.java` → `loadWorkDetail()`
|
||||
- 模型类: `WorksResponse.java`
|
||||
|
||||
3. **POST /api/front/works/update** - 编辑作品 ✨ Android端已接入
|
||||
- 请求参数: `{ "id": 作品ID, "title": "标题", "description": "描述" }`
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.updateWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `editWork()`
|
||||
- 说明: 仅作者可编辑
|
||||
|
||||
4. **POST /api/front/works/delete/{worksId}** - 删除作品 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.deleteWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `deleteWork()`
|
||||
- 说明: 仅作者可删除,逻辑删除
|
||||
|
||||
5. **POST /api/front/works/search** - 搜索作品 ✨ Android端已接入
|
||||
- 请求参数: `{ "keyword": "关键词", "type": "IMAGE/VIDEO", "page": 1, "pageSize": 20 }`
|
||||
- 响应数据: 分页作品列表
|
||||
- 后端实现: `WorksController.searchWorks()`
|
||||
- Android实现: `ApiService.searchWorks()`
|
||||
- 模型类: `WorksSearchRequest.java`
|
||||
|
||||
6. **GET /api/front/works/user/{userId}** - 获取用户作品列表 ✨ Android端已接入
|
||||
- 请求参数: `userId` (路径参数), `page`, `pageSize`
|
||||
- 响应数据: 分页作品列表
|
||||
- 后端实现: `WorksController.getUserWorks()`
|
||||
- Android实现: `ApiService.getUserWorks()`
|
||||
|
||||
7. **POST /api/front/works/like/{worksId}** - 点赞作品 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.likeWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `toggleLike()`
|
||||
- 说明: 需要登录,防止重复点赞
|
||||
|
||||
8. **POST /api/front/works/unlike/{worksId}** - 取消点赞 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.unlikeWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `toggleLike()`
|
||||
|
||||
9. **POST /api/front/works/collect/{worksId}** - 收藏作品 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.collectWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `toggleFavorite()`
|
||||
- 说明: 需要登录,防止重复收藏
|
||||
|
||||
10. **POST /api/front/works/uncollect/{worksId}** - 取消收藏 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.uncollectWorks()`
|
||||
- Android实现: `WorkDetailActivity.java` → `toggleFavorite()`
|
||||
|
||||
11. **GET /api/front/works/my/liked** - 我的点赞列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 分页作品列表
|
||||
- 后端实现: `WorksController.getMyLikedWorks()`
|
||||
- Android实现: `ApiService.getMyLikedWorks()`
|
||||
- 说明: 需要登录
|
||||
|
||||
12. **GET /api/front/works/my/collected** - 我的收藏列表 ✨ Android端已接入
|
||||
- 请求参数: `page`, `pageSize`
|
||||
- 响应数据: 分页作品列表
|
||||
- 后端实现: `WorksController.getMyCollectedWorks()`
|
||||
- Android实现: `ApiService.getMyCollectedWorks()`
|
||||
- 说明: 需要登录
|
||||
|
||||
13. **POST /api/front/works/share/{worksId}** - 分享作品 ✨ Android端已接入
|
||||
- 请求参数: `worksId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "data": true }`
|
||||
- 后端实现: `WorksController.shareWorks()`
|
||||
- Android实现: `ApiService.shareWork()`
|
||||
- 说明: 增加分享次数统计
|
||||
|
||||
14. **POST /api/front/upload/work/video** - 视频上传 ✨ Android端已接入
|
||||
- 请求参数: `multipart/form-data` - `multipart`: 视频文件, `model`: "works", `pid`: 0
|
||||
- 响应数据: `{ "url": "视频URL", "name": "文件名", "size": 文件大小 }`
|
||||
- 后端实现: `UserUploadController.videoUpload()`
|
||||
- Android实现: `PublishWorkActivity.java` → `uploadVideo()`
|
||||
- 说明: 支持大文件上传
|
||||
|
||||
15. **POST /api/front/upload/image** - 图片上传 ✨ Android端已接入
|
||||
- 请求参数: `multipart/form-data` - `multipart`: 图片文件, `model`: "works", `pid`: 0
|
||||
- 响应数据: `{ "url": "图片URL", "name": "文件名", "size": 文件大小 }`
|
||||
- 后端实现: `UserUploadController.image()`
|
||||
- Android实现: `PublishWorkActivity.java` → `uploadCoverImage()`
|
||||
- 说明: 支持多种图片格式
|
||||
|
||||
#### 14. 评论功能 (8个接口) - ✅ 全部完成
|
||||
```
|
||||
✅ POST /api/front/works/comment/publish # 发布评论
|
||||
|
|
@ -206,7 +533,85 @@
|
|||
✅ GET /api/front/works/comment/check-liked/{commentId} # 检查点赞
|
||||
```
|
||||
|
||||
#### 15. 搜索功能 (9个接口) - ✅ 全部完成
|
||||
#### 15. 搜索功能 (9个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ GET /api/front/search/users # 搜索用户 (✨已接入)
|
||||
✅ GET /api/front/search/live-rooms # 搜索直播间 (✨已接入)
|
||||
✅ GET /api/front/search/works # 搜索作品 (✨已接入)
|
||||
✅ GET /api/front/search/all # 综合搜索 (✨已接入)
|
||||
✅ GET /api/front/search/hot # 热门搜索 (✨已接入)
|
||||
✅ GET /api/front/search/history # 搜索历史 (✨已接入)
|
||||
✅ DELETE /api/front/search/history # 清除历史 (✨已接入)
|
||||
✅ DELETE /api/front/search/history/{id} # 删除单条历史 (✨已接入)
|
||||
✅ GET /api/front/search/suggestions # 搜索建议 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **GET /api/front/search/users** - 搜索用户 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (搜索关键词), `pageNum`, `pageSize`
|
||||
- 响应数据: 用户列表,包含关注状态(如果已登录)
|
||||
- 后端实现: `SearchController.searchUsers()`
|
||||
- Android实现: `ApiService.searchUsersGlobal()`
|
||||
- 说明: 支持按昵称或手机号搜索,未登录用户也可访问
|
||||
|
||||
2. **GET /api/front/search/live-rooms** - 搜索直播间 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (搜索关键词), `categoryId` (可选), `isLive` (可选), `pageNum`, `pageSize`
|
||||
- 响应数据: 直播间列表,包含直播状态、观看人数等
|
||||
- 后端实现: `SearchController.searchLiveRooms()`
|
||||
- Android实现: `SearchActivity.java` → `performSearch()`
|
||||
- 说明: 支持按标题或主播名搜索,可按分类和直播状态筛选
|
||||
|
||||
3. **GET /api/front/search/works** - 搜索作品 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (搜索关键词), `categoryId` (可选), `pageNum`, `pageSize`
|
||||
- 响应数据: 作品列表,包含点赞收藏状态(如果已登录)
|
||||
- 后端实现: `SearchController.searchWorks()`
|
||||
- Android实现: `ApiService.searchWorksGlobal()`
|
||||
- 说明: 支持按标题、描述、标签搜索
|
||||
|
||||
4. **GET /api/front/search/all** - 综合搜索 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (搜索关键词)
|
||||
- 响应数据: `{ "users": [], "liveRooms": [], "works": [] }`
|
||||
- 后端实现: `SearchController.searchAll()`
|
||||
- Android实现: `ApiService.searchAll()`
|
||||
- 说明: 同时搜索用户、直播间、作品,返回各类型的前几条结果
|
||||
|
||||
5. **GET /api/front/search/hot** - 获取热门搜索 ✨ Android端已接入
|
||||
- 请求参数: `searchType` (0-全部 1-用户 2-直播间 3-作品), `limit`
|
||||
- 响应数据: `[{ "keyword": "关键词", "searchCount": 次数 }]`
|
||||
- 后端实现: `SearchController.getHotSearch()`
|
||||
- Android实现: `SearchActivity.java` → `loadHotSearch()`
|
||||
- 说明: 获取热门搜索关键词列表,支持按类型筛选
|
||||
|
||||
6. **GET /api/front/search/history** - 获取搜索历史 ✨ Android端已接入
|
||||
- 请求参数: `searchType` (可选), `limit`
|
||||
- 响应数据: `[{ "id": ID, "keyword": "关键词", "searchType": 类型, "createTime": "时间" }]`
|
||||
- 后端实现: `SearchController.getSearchHistory()`
|
||||
- Android实现: `ApiService.getSearchHistory()`
|
||||
- 说明: 获取用户的搜索历史记录,需要登录
|
||||
|
||||
7. **DELETE /api/front/search/history** - 清除搜索历史 ✨ Android端已接入
|
||||
- 请求参数: `searchType` (可选,不传则清除全部)
|
||||
- 响应数据: `{ "code": 200, "msg": "搜索历史已清除" }`
|
||||
- 后端实现: `SearchController.clearSearchHistory()`
|
||||
- Android实现: `ApiService.clearSearchHistory()`
|
||||
- 说明: 清除全部或指定类型的搜索历史,需要登录
|
||||
|
||||
8. **DELETE /api/front/search/history/{historyId}** - 删除单条搜索历史 ✨ Android端已接入
|
||||
- 请求参数: `historyId` (路径参数)
|
||||
- 响应数据: `{ "code": 200, "msg": "删除成功" }`
|
||||
- 后端实现: `SearchController.deleteSearchHistory()`
|
||||
- Android实现: `ApiService.deleteSearchHistory()`
|
||||
- 说明: 删除指定的搜索历史记录,需要登录
|
||||
|
||||
9. **GET /api/front/search/suggestions** - 获取搜索建议 ✨ Android端已接入
|
||||
- 请求参数: `keyword` (关键词前缀), `searchType` (可选), `limit`
|
||||
- 响应数据: `["建议1", "建议2", ...]`
|
||||
- 后端实现: `SearchController.getSearchSuggestions()`
|
||||
- Android实现: `SearchActivity.java` → `loadSearchSuggestions()`
|
||||
- 说明: 根据用户输入提供自动补全建议,需要登录
|
||||
|
||||
#### 16. 通知推送 (9个接口) - ✅ 全部完成
|
||||
```
|
||||
✅ GET /api/front/search/users # 搜索用户
|
||||
✅ GET /api/front/search/live-rooms # 搜索直播间
|
||||
|
|
@ -234,14 +639,65 @@
|
|||
✅ GET /api/front/notification/unread-count-by-type # 按类型统计
|
||||
```
|
||||
|
||||
#### 17. 支付集成 (4个接口) - ✅ 全部完成
|
||||
#### 17. 支付集成 (4个接口) - ✅ 已完成对接 ✨ Android端已接入
|
||||
```
|
||||
✅ POST /api/front/pay/payment # 创建支付
|
||||
✅ GET /api/front/pay/alipay/queryPayResult # 查询结果
|
||||
✅ GET /api/front/pay/alipay/return # 支付返回
|
||||
✅ POST /api/admin/payment/callback/alipay # 支付回调
|
||||
✅ GET /api/front/gift/recharge/options # 获取充值选项 (✨已接入)
|
||||
✅ POST /api/front/gift/recharge/create # 创建充值订单 (✨已接入)
|
||||
✅ POST /api/front/pay/payment # 订单支付 (✨已接入)
|
||||
✅ GET /api/front/pay/alipay/queryPayResult # 查询支付结果 (✨已接入)
|
||||
```
|
||||
|
||||
**接口详细说明**:
|
||||
|
||||
1. **GET /api/front/gift/recharge/options** - 获取充值选项列表 ✨ Android端已接入
|
||||
- 请求参数: 无
|
||||
- 响应数据: `[{ "id": "选项ID", "coinAmount": 金币数量, "price": 价格, "discountLabel": "优惠标签" }]`
|
||||
- 后端实现: `GiftController.getRechargeOptions()`
|
||||
- Android实现: `RoomDetailActivity.java` → `loadRechargeOptions()`
|
||||
- 模型类: `RechargeOptionResponse.java`
|
||||
- 说明: 获取所有启用的充值选项,包含金币数量、价格和优惠标签
|
||||
|
||||
2. **POST /api/front/gift/recharge/create** - 创建充值订单 ✨ Android端已接入
|
||||
- 请求参数: `{ "optionId": 选项ID, "coinAmount": 金币数量, "price": 价格 }`
|
||||
- 响应数据: `{ "orderId": "订单ID", "paymentUrl": "支付URL" }`
|
||||
- 后端实现: `GiftController.createRecharge()`
|
||||
- Android实现: `RoomDetailActivity.java` → `createRechargeOrder()`
|
||||
- 模型类: `CreateRechargeRequest.java`, `CreateRechargeResponse.java`
|
||||
- 说明: 创建充值订单,返回订单ID和支付URL,用于后续支付
|
||||
|
||||
3. **POST /api/front/pay/payment** - 订单支付 ✨ Android端已接入
|
||||
- 请求参数: `{ "orderNo": "订单号", "payType": "支付类型", "payChannel": "支付渠道", "from": "android" }`
|
||||
- 响应数据: `{ "status": true/false, "payType": "支付类型", "orderNo": "订单号", "jsConfig": {...} }`
|
||||
- 后端实现: `PayController.payment()`
|
||||
- Android实现: `RoomDetailActivity.java` → `processPayment()`
|
||||
- 模型类: `OrderPayRequest.java`, `OrderPayResultResponse.java`
|
||||
- 说明: 发起支付,支持微信支付(weixin/weixinAppAndroid)、支付宝(alipay/appAliPay)、余额支付(yue)
|
||||
- 注意: 需要集成微信支付SDK和支付宝SDK才能完成实际支付
|
||||
|
||||
4. **GET /api/front/pay/alipay/queryPayResult** - 查询支付宝支付结果 ✨ Android端已接入
|
||||
- 请求参数: `orderNo` (订单号)
|
||||
- 响应数据: `{ "code": 200, "data": true/false }`
|
||||
- 后端实现: `PayController.queryAliPayResult()`
|
||||
- Android实现: `ApiService.queryAliPayResult()`
|
||||
- 说明: 查询支付宝支付结果,用于确认支付是否成功
|
||||
|
||||
**支付流程说明**:
|
||||
1. 用户点击充值按钮,显示充值对话框
|
||||
2. 调用 `getRechargeOptions()` 获取充值选项列表
|
||||
3. 用户选择充值金额,点击确认
|
||||
4. 调用 `createRecharge()` 创建充值订单,获取订单ID
|
||||
5. 用户选择支付方式(支付宝/微信/余额)
|
||||
6. 调用 `payment()` 发起支付,获取支付参数
|
||||
7. 调用支付SDK完成支付(需要集成微信/支付宝SDK)
|
||||
8. 支付完成后调用 `queryPayResult()` 查询支付结果
|
||||
9. 支付成功后更新用户金币余额
|
||||
|
||||
**待完成工作**:
|
||||
- 集成微信支付SDK(需要微信开放平台账号和配置)
|
||||
- 集成支付宝SDK(需要支付宝开放平台账号和配置)
|
||||
- 实现支付结果回调处理
|
||||
- 实现支付成功后的余额更新逻辑
|
||||
|
||||
#### 18. 文件上传 (5个接口) - ✅ 全部完成
|
||||
```
|
||||
✅ POST /api/upload/image # 图片上传
|
||||
|
|
@ -356,7 +812,7 @@
|
|||
| 评论功能 | 8 | ✅ 100% | 2024-12-29 |
|
||||
| 搜索功能 | 9 | ✅ 100% | 2024-12-29 |
|
||||
| 通知推送 | 9 | ✅ 100% | 2024-12-29 |
|
||||
| 支付集成 | 4 | ✅ 100% | 2024-12-29 |
|
||||
| 支付集成 | 4 | ✅ 100% | 2024-12-29 ✨ Android端已接入 |
|
||||
| 文件上传 | 5 | ✅ 100% | 2024-12-29 |
|
||||
| 分类管理 | 7 | ✅ 100% | 2024-12-29 |
|
||||
| **总计** | **131** | **✅ 100%** | **2024-12-29** |
|
||||
|
|
@ -454,29 +910,4 @@
|
|||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## ✅ 完成清单
|
||||
|
||||
- [x] 用户认证模块 (3个接口)
|
||||
- [x] 用户资料模块 (4个接口)
|
||||
- [x] 直播间管理模块 (10个接口)
|
||||
- [x] 直播间弹幕模块 (2个接口)
|
||||
- [x] WebSocket通信模块 (2个连接)
|
||||
- [x] 好友管理模块 (9个接口)
|
||||
- [x] 群组管理模块 (10个接口)
|
||||
- [x] 群组消息模块 (4个接口)
|
||||
- [x] 消息聊天模块 (12个接口)
|
||||
- [x] 消息表情回应模块 (4个接口)
|
||||
- [x] 消息搜索模块 (3个接口)
|
||||
- [x] 关注功能模块 (8个接口)
|
||||
- [x] 礼物管理模块 (4个接口)
|
||||
- [x] 作品管理模块 (15个接口)
|
||||
- [x] 评论功能模块 (8个接口)
|
||||
- [x] 搜索功能模块 (9个接口)
|
||||
- [x] 通知推送模块 (9个接口)
|
||||
- [x] 支付集成模块 (4个接口)
|
||||
- [x] 文件上传模块 (5个接口)
|
||||
- [x] 分类管理模块 (7个接口)
|
||||
|
||||
**总计**: 131个接口,全部完成 ✅
|
||||
|
|
@ -282,4 +282,95 @@ public class FriendController {
|
|||
return CommonResult.failed("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉黑好友
|
||||
*/
|
||||
@ApiOperation(value = "拉黑好友")
|
||||
@PostMapping("/friends/block/{friendId}")
|
||||
public CommonResult<Boolean> blockFriend(@PathVariable Integer friendId) {
|
||||
Integer currentUserId = userService.getUserId();
|
||||
|
||||
if (friendId == null || friendId.equals(currentUserId)) {
|
||||
return CommonResult.failed("无效的用户ID");
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否已经拉黑
|
||||
String checkSql = "SELECT COUNT(*) FROM eb_user_blacklist WHERE user_id = ? AND blocked_user_id = ?";
|
||||
Long count = jdbcTemplate.queryForObject(checkSql, Long.class, currentUserId, friendId);
|
||||
if (count != null && count > 0) {
|
||||
return CommonResult.failed("该用户已在黑名单中");
|
||||
}
|
||||
|
||||
// 添加到黑名单
|
||||
String insertSql = "INSERT INTO eb_user_blacklist (user_id, blocked_user_id, create_time) VALUES (?, ?, NOW())";
|
||||
jdbcTemplate.update(insertSql, currentUserId, friendId);
|
||||
|
||||
// 删除好友关系(如果存在)
|
||||
String deleteFriendSql = "DELETE FROM eb_friend WHERE " +
|
||||
"(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)";
|
||||
jdbcTemplate.update(deleteFriendSql, currentUserId, friendId, friendId, currentUserId);
|
||||
|
||||
return CommonResult.success(true);
|
||||
} catch (Exception e) {
|
||||
log.error("拉黑好友失败: {}", e.getMessage());
|
||||
return CommonResult.failed("拉黑失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消拉黑
|
||||
*/
|
||||
@ApiOperation(value = "取消拉黑")
|
||||
@PostMapping("/friends/unblock/{friendId}")
|
||||
public CommonResult<Boolean> unblockFriend(@PathVariable Integer friendId) {
|
||||
Integer currentUserId = userService.getUserId();
|
||||
|
||||
try {
|
||||
String deleteSql = "DELETE FROM eb_user_blacklist WHERE user_id = ? AND blocked_user_id = ?";
|
||||
int rows = jdbcTemplate.update(deleteSql, currentUserId, friendId);
|
||||
if (rows > 0) {
|
||||
return CommonResult.success(true);
|
||||
} else {
|
||||
return CommonResult.failed("该用户不在黑名单中");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("取消拉黑失败: {}", e.getMessage());
|
||||
return CommonResult.failed("取消拉黑失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取黑名单列表
|
||||
*/
|
||||
@ApiOperation(value = "获取黑名单列表")
|
||||
@GetMapping("/friends/blocked")
|
||||
public CommonResult<CommonPage<Map<String, Object>>> getBlockedList(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
|
||||
Integer currentUserId = userService.getUserId();
|
||||
|
||||
String sql = "SELECT u.uid as id, u.nickname as name, u.avatar as avatarUrl, u.phone, " +
|
||||
"b.create_time as blockedTime " +
|
||||
"FROM eb_user_blacklist b " +
|
||||
"JOIN eb_user u ON b.blocked_user_id = u.uid " +
|
||||
"WHERE b.user_id = ? " +
|
||||
"ORDER BY b.create_time DESC LIMIT ?, ?";
|
||||
|
||||
String countSql = "SELECT COUNT(*) FROM eb_user_blacklist WHERE user_id = ?";
|
||||
Long total = jdbcTemplate.queryForObject(countSql, Long.class, currentUserId);
|
||||
|
||||
int offset = (page - 1) * pageSize;
|
||||
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, currentUserId, offset, pageSize);
|
||||
|
||||
CommonPage<Map<String, Object>> result = new CommonPage<>();
|
||||
result.setList(list);
|
||||
result.setTotal(total != null ? total : 0L);
|
||||
result.setPage(page);
|
||||
result.setLimit(pageSize);
|
||||
|
||||
return CommonResult.success(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ dependencies {
|
|||
|
||||
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||
|
||||
// FlexboxLayout for message reactions
|
||||
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import com.example.livestreaming.net.MessageReaction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ChatMessage {
|
||||
|
|
@ -31,6 +35,9 @@ public class ChatMessage {
|
|||
private int voiceDuration; // 语音时长(秒)
|
||||
private int imageWidth; // 图片宽度
|
||||
private int imageHeight; // 图片高度
|
||||
|
||||
// 表情回应相关字段
|
||||
private List<MessageReaction> reactions; // 表情回应列表
|
||||
|
||||
// 文本消息构造函数
|
||||
public ChatMessage(String username, String message) {
|
||||
|
|
@ -212,4 +219,25 @@ public class ChatMessage {
|
|||
public void setImageHeight(int imageHeight) {
|
||||
this.imageHeight = imageHeight;
|
||||
}
|
||||
|
||||
public List<MessageReaction> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
|
||||
public void setReactions(List<MessageReaction> reactions) {
|
||||
this.reactions = reactions;
|
||||
}
|
||||
|
||||
public void addReaction(MessageReaction reaction) {
|
||||
if (this.reactions == null) {
|
||||
this.reactions = new ArrayList<>();
|
||||
}
|
||||
this.reactions.add(reaction);
|
||||
}
|
||||
|
||||
public void removeReaction(String emoji) {
|
||||
if (this.reactions != null) {
|
||||
this.reactions.removeIf(r -> r.getEmoji().equals(emoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -374,6 +374,7 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
private void showMessageMenu(ChatMessage message, int position, View anchorView) {
|
||||
PopupMenu popupMenu = new PopupMenu(this, anchorView);
|
||||
popupMenu.getMenu().add(0, 0, 0, "复制");
|
||||
popupMenu.getMenu().add(0, 2, 0, "表情回应");
|
||||
// 只有自己发送的消息才能删除
|
||||
if ("我".equals(message.getUsername())) {
|
||||
popupMenu.getMenu().add(0, 1, 0, "删除");
|
||||
|
|
@ -386,6 +387,9 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
} else if (item.getItemId() == 1) {
|
||||
deleteMessage(message, position);
|
||||
return true;
|
||||
} else if (item.getItemId() == 2) {
|
||||
showEmojiPicker(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
@ -733,4 +737,194 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
stopPolling();
|
||||
handler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示表情选择器
|
||||
*/
|
||||
private void showEmojiPicker(ChatMessage message) {
|
||||
EmojiPickerBottomSheet emojiPicker = EmojiPickerBottomSheet.newInstance();
|
||||
emojiPicker.setOnEmojiSelectedListener(emoji -> {
|
||||
addMessageReaction(message, emoji);
|
||||
});
|
||||
emojiPicker.show(getSupportFragmentManager(), "emoji_picker");
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加表情回应
|
||||
*/
|
||||
private void addMessageReaction(ChatMessage message, String emoji) {
|
||||
if (!AuthHelper.requireLoginWithToast(this, "添加表情回应需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null || message.getMessageId() == null) {
|
||||
Snackbar.make(binding.getRoot(), "无法添加表情回应", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/messages/reactions/add";
|
||||
Log.d(TAG, "添加表情回应: " + url);
|
||||
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("messageId", message.getMessageId());
|
||||
body.put("emoji", emoji);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "添加表情回应失败", e);
|
||||
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "添加表情回应失败", Snackbar.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
Log.d(TAG, "添加表情回应响应: " + responseBody);
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
Snackbar.make(binding.getRoot(), "已添加表情回应", Snackbar.LENGTH_SHORT).show();
|
||||
// 重新加载消息以更新表情回应
|
||||
loadMessageReactions(message);
|
||||
} else {
|
||||
String msg = json.optString("message", "添加失败");
|
||||
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "构建请求失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除表情回应
|
||||
*/
|
||||
private void removeMessageReaction(ChatMessage message, String emoji) {
|
||||
if (!AuthHelper.requireLoginWithToast(this, "移除表情回应需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null || message.getMessageId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/messages/reactions/remove";
|
||||
Log.d(TAG, "移除表情回应: " + url);
|
||||
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("messageId", message.getMessageId());
|
||||
body.put("emoji", emoji);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.method("DELETE", RequestBody.create(body.toString(), MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "移除表情回应失败", e);
|
||||
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "移除表情回应失败", Snackbar.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
Log.d(TAG, "移除表情回应响应: " + responseBody);
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
Snackbar.make(binding.getRoot(), "已移除表情回应", Snackbar.LENGTH_SHORT).show();
|
||||
// 重新加载消息以更新表情回应
|
||||
loadMessageReactions(message);
|
||||
} else {
|
||||
String msg = json.optString("message", "移除失败");
|
||||
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "构建请求失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载消息的表情回应列表
|
||||
*/
|
||||
private void loadMessageReactions(ChatMessage message) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null || message.getMessageId() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/messages/" + message.getMessageId() + "/reactions";
|
||||
Log.d(TAG, "加载表情回应: " + url);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "加载表情回应失败", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
Log.d(TAG, "表情回应响应: " + responseBody);
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
JSONArray data = json.optJSONArray("data");
|
||||
if (data != null) {
|
||||
List<com.example.livestreaming.net.MessageReaction> reactions = new ArrayList<>();
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
JSONObject item = data.getJSONObject(i);
|
||||
String emoji = item.optString("emoji", "");
|
||||
int count = item.optInt("count", 0);
|
||||
boolean reactedByMe = item.optBoolean("reactedByMe", false);
|
||||
reactions.add(new com.example.livestreaming.net.MessageReaction(emoji, count, reactedByMe));
|
||||
}
|
||||
message.setReactions(reactions);
|
||||
// 更新适配器
|
||||
int position = messages.indexOf(message);
|
||||
if (position >= 0) {
|
||||
adapter.notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析表情回应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
private final TextView nameText;
|
||||
private final TextView msgText;
|
||||
private final TextView timeText;
|
||||
private final com.google.android.flexbox.FlexboxLayout reactionsContainer;
|
||||
|
||||
IncomingTextVH(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
|
@ -149,6 +150,7 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
nameText = itemView.findViewById(R.id.nameText);
|
||||
msgText = itemView.findViewById(R.id.messageText);
|
||||
timeText = itemView.findViewById(R.id.timeText);
|
||||
reactionsContainer = itemView.findViewById(R.id.reactionsContainer);
|
||||
setupAvatarOutline();
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +177,9 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
|
||||
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
|
||||
|
||||
// 显示表情回应
|
||||
displayReactions(message);
|
||||
|
||||
// 清除之前的监听器,避免重复绑定
|
||||
itemView.setOnClickListener(null);
|
||||
itemView.setOnLongClickListener(null);
|
||||
|
|
@ -197,6 +202,39 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
loadAvatar(message);
|
||||
}
|
||||
|
||||
private void displayReactions(ChatMessage message) {
|
||||
if (reactionsContainer == null) return;
|
||||
|
||||
reactionsContainer.removeAllViews();
|
||||
|
||||
if (message.getReactions() == null || message.getReactions().isEmpty()) {
|
||||
reactionsContainer.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
for (com.example.livestreaming.net.MessageReaction reaction : message.getReactions()) {
|
||||
View reactionView = LayoutInflater.from(itemView.getContext())
|
||||
.inflate(R.layout.item_message_reaction, reactionsContainer, false);
|
||||
|
||||
TextView emojiText = reactionView.findViewById(R.id.emojiText);
|
||||
TextView countText = reactionView.findViewById(R.id.countText);
|
||||
|
||||
emojiText.setText(reaction.getEmoji());
|
||||
countText.setText(String.valueOf(reaction.getCount()));
|
||||
|
||||
// 如果当前用户已回应,高亮显示
|
||||
if (reaction.isReactedByMe()) {
|
||||
reactionView.setBackgroundResource(R.drawable.reaction_background_selected);
|
||||
} else {
|
||||
reactionView.setBackgroundResource(R.drawable.reaction_background);
|
||||
}
|
||||
|
||||
reactionsContainer.addView(reactionView);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAvatar(ChatMessage message) {
|
||||
// TODO: 接入后端接口 - 加载消息发送者头像
|
||||
// 接口路径: GET /api/users/{userId}/avatar 或直接从ChatMessage的avatarUrl字段获取
|
||||
|
|
@ -263,6 +301,7 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
private final TextView msgText;
|
||||
private final TextView timeText;
|
||||
private final ImageView statusIcon;
|
||||
private final com.google.android.flexbox.FlexboxLayout reactionsContainer;
|
||||
|
||||
OutgoingTextVH(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
|
@ -270,6 +309,7 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
msgText = itemView.findViewById(R.id.messageText);
|
||||
timeText = itemView.findViewById(R.id.timeText);
|
||||
statusIcon = itemView.findViewById(R.id.statusIcon);
|
||||
reactionsContainer = itemView.findViewById(R.id.reactionsContainer);
|
||||
setupAvatarOutline();
|
||||
}
|
||||
|
||||
|
|
@ -295,6 +335,9 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
|
||||
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
|
||||
|
||||
// 显示表情回应
|
||||
displayReactions(message);
|
||||
|
||||
// 显示消息状态图标(仅对发送的消息显示)
|
||||
if (statusIcon != null && message.getStatus() != null) {
|
||||
switch (message.getStatus()) {
|
||||
|
|
@ -341,6 +384,39 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
loadAvatar();
|
||||
}
|
||||
|
||||
private void displayReactions(ChatMessage message) {
|
||||
if (reactionsContainer == null) return;
|
||||
|
||||
reactionsContainer.removeAllViews();
|
||||
|
||||
if (message.getReactions() == null || message.getReactions().isEmpty()) {
|
||||
reactionsContainer.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
for (com.example.livestreaming.net.MessageReaction reaction : message.getReactions()) {
|
||||
View reactionView = LayoutInflater.from(itemView.getContext())
|
||||
.inflate(R.layout.item_message_reaction, reactionsContainer, false);
|
||||
|
||||
TextView emojiText = reactionView.findViewById(R.id.emojiText);
|
||||
TextView countText = reactionView.findViewById(R.id.countText);
|
||||
|
||||
emojiText.setText(reaction.getEmoji());
|
||||
countText.setText(String.valueOf(reaction.getCount()));
|
||||
|
||||
// 如果当前用户已回应,高亮显示
|
||||
if (reaction.isReactedByMe()) {
|
||||
reactionView.setBackgroundResource(R.drawable.reaction_background_selected);
|
||||
} else {
|
||||
reactionView.setBackgroundResource(R.drawable.reaction_background);
|
||||
}
|
||||
|
||||
reactionsContainer.addView(reactionView);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAvatar() {
|
||||
if (avatarView == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
/**
|
||||
* 表情选择器底部弹窗
|
||||
*/
|
||||
public class EmojiPickerBottomSheet extends BottomSheetDialogFragment {
|
||||
|
||||
public interface OnEmojiSelectedListener {
|
||||
void onEmojiSelected(String emoji);
|
||||
}
|
||||
|
||||
private OnEmojiSelectedListener listener;
|
||||
|
||||
// 常用表情列表
|
||||
private static final String[] EMOJIS = {
|
||||
"👍", "❤️", "😂", "😮", "😢", "😠",
|
||||
"🔥", "👏", "🤔", "🎉", "⭐", "✅"
|
||||
};
|
||||
|
||||
private static final int[] EMOJI_IDS = {
|
||||
R.id.emoji_like, R.id.emoji_love, R.id.emoji_laugh,
|
||||
R.id.emoji_wow, R.id.emoji_sad, R.id.emoji_angry,
|
||||
R.id.emoji_fire, R.id.emoji_clap, R.id.emoji_thinking,
|
||||
R.id.emoji_party, R.id.emoji_star, R.id.emoji_check
|
||||
};
|
||||
|
||||
public static EmojiPickerBottomSheet newInstance() {
|
||||
return new EmojiPickerBottomSheet();
|
||||
}
|
||||
|
||||
public void setOnEmojiSelectedListener(OnEmojiSelectedListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.bottom_sheet_emoji_picker, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setupEmojiButtons(view);
|
||||
}
|
||||
|
||||
private void setupEmojiButtons(View view) {
|
||||
for (int i = 0; i < EMOJI_IDS.length && i < EMOJIS.length; i++) {
|
||||
TextView emojiView = view.findViewById(EMOJI_IDS[i]);
|
||||
if (emojiView != null) {
|
||||
final String emoji = EMOJIS[i];
|
||||
emojiView.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onEmojiSelected(emoji);
|
||||
}
|
||||
dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,32 @@ package com.example.livestreaming;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityFansListBinding;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.PageResponse;
|
||||
import com.example.livestreaming.net.RetrofitClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class FansListActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityFansListBinding binding;
|
||||
private FriendsAdapter adapter;
|
||||
private int currentPage = 1;
|
||||
private boolean isLoading = false;
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, FansListActivity.class);
|
||||
|
|
@ -30,33 +43,72 @@ public class FansListActivity extends AppCompatActivity {
|
|||
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
FriendsAdapter adapter = new FriendsAdapter(item -> {
|
||||
adapter = new FriendsAdapter(item -> {
|
||||
if (item == null) return;
|
||||
Toast.makeText(this, "打开粉丝:" + item.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取粉丝列表
|
||||
// 接口路径: GET /api/fans
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive, followTime等字段
|
||||
// 列表应按关注时间倒序排列(最新关注的在前)
|
||||
binding.fansRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.fansRecyclerView.setAdapter(adapter);
|
||||
adapter.submitList(buildDemoFans());
|
||||
|
||||
loadFollowersList();
|
||||
}
|
||||
|
||||
private List<FriendItem> buildDemoFans() {
|
||||
List<FriendItem> list = new ArrayList<>();
|
||||
list.add(new FriendItem("f1", "小雨", "关注了你 · 2分钟前", true));
|
||||
list.add(new FriendItem("f2", "阿宁", "关注了你 · 昨天", false));
|
||||
list.add(new FriendItem("f3", "小星", "关注了你 · 周二", true));
|
||||
list.add(new FriendItem("f4", "小林", "关注了你 · 上周", false));
|
||||
list.add(new FriendItem("f5", "阿杰", "关注了你 · 上周", false));
|
||||
list.add(new FriendItem("f6", "小七", "关注了你 · 上月", true));
|
||||
return list;
|
||||
private void loadFollowersList() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowersList(currentPage, 20);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
|
||||
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
|
||||
isLoading = false;
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<PageResponse<Map<String, Object>>> apiResponse = response.body();
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
PageResponse<Map<String, Object>> pageData = apiResponse.getData();
|
||||
List<Map<String, Object>> followersList = pageData.getList();
|
||||
|
||||
if (followersList != null && !followersList.isEmpty()) {
|
||||
List<FriendItem> items = new ArrayList<>();
|
||||
for (Map<String, Object> user : followersList) {
|
||||
String id = String.valueOf(user.get("userId"));
|
||||
String name = (String) user.get("nickname");
|
||||
String phone = (String) user.get("phone");
|
||||
Boolean isOnline = (Boolean) user.get("isOnline");
|
||||
Boolean isMutualFollow = (Boolean) user.get("isMutualFollow");
|
||||
|
||||
String status = isMutualFollow != null && isMutualFollow ?
|
||||
"互相关注" : (isOnline != null && isOnline ? "在线" : "离线");
|
||||
items.add(new FriendItem(id, name != null ? name : phone, status,
|
||||
isOnline != null && isOnline));
|
||||
}
|
||||
adapter.submitList(items);
|
||||
} else {
|
||||
// 空列表
|
||||
adapter.submitList(new ArrayList<>());
|
||||
Toast.makeText(FansListActivity.this, "暂无粉丝", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(FansListActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取粉丝列表失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(FansListActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
|
||||
isLoading = false;
|
||||
Toast.makeText(FansListActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,32 @@ package com.example.livestreaming;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityFollowingListBinding;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.PageResponse;
|
||||
import com.example.livestreaming.net.RetrofitClient;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class FollowingListActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityFollowingListBinding binding;
|
||||
private FriendsAdapter adapter;
|
||||
private int currentPage = 1;
|
||||
private boolean isLoading = false;
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, FollowingListActivity.class);
|
||||
|
|
@ -30,32 +43,70 @@ public class FollowingListActivity extends AppCompatActivity {
|
|||
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
FriendsAdapter adapter = new FriendsAdapter(item -> {
|
||||
adapter = new FriendsAdapter(item -> {
|
||||
if (item == null) return;
|
||||
Toast.makeText(this, "打开关注:" + item.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// TODO: 接入后端接口 - 获取关注列表
|
||||
// 接口路径: GET /api/following
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive, lastLiveTime, followTime等字段
|
||||
// 列表应按关注时间倒序或最后直播时间倒序排列
|
||||
binding.followingRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.followingRecyclerView.setAdapter(adapter);
|
||||
adapter.submitList(buildDemoFollowing());
|
||||
|
||||
loadFollowingList();
|
||||
}
|
||||
|
||||
private List<FriendItem> buildDemoFollowing() {
|
||||
List<FriendItem> list = new ArrayList<>();
|
||||
list.add(new FriendItem("fo1", "王者荣耀陪练", "主播 · 正在直播", true));
|
||||
list.add(new FriendItem("fo2", "音乐电台", "主播 · 今日 20:00 开播", false));
|
||||
list.add(new FriendItem("fo3", "户外阿杰", "主播 · 1小时前开播", true));
|
||||
list.add(new FriendItem("fo4", "美食探店", "主播 · 昨天直播", false));
|
||||
list.add(new FriendItem("fo5", "聊天小七", "主播 · 正在直播", true));
|
||||
return list;
|
||||
private void loadFollowingList() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> call = apiService.getFollowingList(currentPage, 20);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
|
||||
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
|
||||
isLoading = false;
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<PageResponse<Map<String, Object>>> apiResponse = response.body();
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
PageResponse<Map<String, Object>> pageData = apiResponse.getData();
|
||||
List<Map<String, Object>> followingList = pageData.getList();
|
||||
|
||||
if (followingList != null && !followingList.isEmpty()) {
|
||||
List<FriendItem> items = new ArrayList<>();
|
||||
for (Map<String, Object> user : followingList) {
|
||||
String id = String.valueOf(user.get("userId"));
|
||||
String name = (String) user.get("nickname");
|
||||
String phone = (String) user.get("phone");
|
||||
Boolean isOnline = (Boolean) user.get("isOnline");
|
||||
|
||||
String status = isOnline != null && isOnline ? "在线" : "离线";
|
||||
items.add(new FriendItem(id, name != null ? name : phone, status,
|
||||
isOnline != null && isOnline));
|
||||
}
|
||||
adapter.submitList(items);
|
||||
} else {
|
||||
// 空列表
|
||||
adapter.submitList(new ArrayList<>());
|
||||
Toast.makeText(FollowingListActivity.this, "暂无关注", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(FollowingListActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取关注列表失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(FollowingListActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
|
||||
isLoading = false;
|
||||
Toast.makeText(FollowingListActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,37 +18,48 @@ public class FriendsAdapter extends ListAdapter<FriendItem, FriendsAdapter.VH> {
|
|||
void onFriendClick(FriendItem item);
|
||||
}
|
||||
|
||||
public interface OnFriendLongClickListener {
|
||||
void onFriendLongClick(FriendItem item, int position);
|
||||
}
|
||||
|
||||
private final OnFriendClickListener onFriendClickListener;
|
||||
private OnFriendLongClickListener onFriendLongClickListener;
|
||||
|
||||
public FriendsAdapter(OnFriendClickListener onFriendClickListener) {
|
||||
super(DIFF);
|
||||
this.onFriendClickListener = onFriendClickListener;
|
||||
}
|
||||
|
||||
public void setOnFriendLongClickListener(OnFriendLongClickListener listener) {
|
||||
this.onFriendLongClickListener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemFriendBinding binding = ItemFriendBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new VH(binding, onFriendClickListener);
|
||||
return new VH(binding, onFriendClickListener, onFriendLongClickListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
holder.bind(getItem(position), position);
|
||||
}
|
||||
|
||||
static class VH extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ItemFriendBinding binding;
|
||||
private final OnFriendClickListener onFriendClickListener;
|
||||
private final OnFriendLongClickListener onFriendLongClickListener;
|
||||
|
||||
VH(ItemFriendBinding binding, OnFriendClickListener onFriendClickListener) {
|
||||
VH(ItemFriendBinding binding, OnFriendClickListener onFriendClickListener, OnFriendLongClickListener onFriendLongClickListener) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
this.onFriendClickListener = onFriendClickListener;
|
||||
this.onFriendLongClickListener = onFriendLongClickListener;
|
||||
}
|
||||
|
||||
void bind(FriendItem item) {
|
||||
void bind(FriendItem item, int position) {
|
||||
if (item == null) return;
|
||||
|
||||
binding.name.setText(item.getName() != null ? item.getName() : "");
|
||||
|
|
@ -74,6 +85,14 @@ public class FriendsAdapter extends ListAdapter<FriendItem, FriendsAdapter.VH> {
|
|||
binding.getRoot().setOnClickListener(v -> {
|
||||
if (onFriendClickListener != null) onFriendClickListener.onFriendClick(item);
|
||||
});
|
||||
|
||||
binding.getRoot().setOnLongClickListener(v -> {
|
||||
if (onFriendLongClickListener != null) {
|
||||
onFriendLongClickListener.onFriendLongClick(item, position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
|
|
@ -36,9 +37,11 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
private ActivityMyFriendsBinding binding;
|
||||
private FriendsAdapter friendsAdapter;
|
||||
private FriendRequestAdapter requestAdapter;
|
||||
private FriendsAdapter blockedAdapter;
|
||||
private final List<FriendItem> allFriends = new ArrayList<>();
|
||||
private final List<FriendRequestItem> allRequests = new ArrayList<>();
|
||||
private int currentTab = 0; // 0: 好友列表, 1: 好友请求
|
||||
private final List<FriendItem> allBlocked = new ArrayList<>();
|
||||
private int currentTab = 0; // 0: 好友列表, 1: 好友请求, 2: 黑名单
|
||||
private OkHttpClient httpClient;
|
||||
|
||||
@Override
|
||||
|
|
@ -71,6 +74,7 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
// 点击好友打开私聊会话
|
||||
openConversation(item);
|
||||
});
|
||||
friendsAdapter.setOnFriendLongClickListener(this::showFriendOptionsDialog);
|
||||
binding.friendsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.friendsRecyclerView.setAdapter(friendsAdapter);
|
||||
|
||||
|
|
@ -90,6 +94,14 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
binding.requestsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.requestsRecyclerView.setAdapter(requestAdapter);
|
||||
|
||||
// 黑名单适配器
|
||||
blockedAdapter = new FriendsAdapter(item -> {
|
||||
// 黑名单项点击不做操作
|
||||
});
|
||||
blockedAdapter.setOnFriendLongClickListener(this::showBlockedOptionsDialog);
|
||||
binding.blockedRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.blockedRecyclerView.setAdapter(blockedAdapter);
|
||||
|
||||
// 搜索框
|
||||
binding.searchEdit.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
|
|
@ -107,6 +119,7 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
private void setupTabs() {
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("好友列表"));
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("好友请求"));
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("黑名单"));
|
||||
|
||||
binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
|
|
@ -125,13 +138,21 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
if (tabIndex == 0) {
|
||||
binding.friendsRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.requestsRecyclerView.setVisibility(View.GONE);
|
||||
binding.blockedRecyclerView.setVisibility(View.GONE);
|
||||
binding.searchContainer.setVisibility(View.VISIBLE);
|
||||
loadFriendList();
|
||||
} else {
|
||||
} else if (tabIndex == 1) {
|
||||
binding.friendsRecyclerView.setVisibility(View.GONE);
|
||||
binding.requestsRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.blockedRecyclerView.setVisibility(View.GONE);
|
||||
binding.searchContainer.setVisibility(View.GONE);
|
||||
loadFriendRequests();
|
||||
} else {
|
||||
binding.friendsRecyclerView.setVisibility(View.GONE);
|
||||
binding.requestsRecyclerView.setVisibility(View.GONE);
|
||||
binding.blockedRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.searchContainer.setVisibility(View.GONE);
|
||||
loadBlockedList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,8 +466,278 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
// 返回时刷新数据
|
||||
if (currentTab == 0) {
|
||||
loadFriendList();
|
||||
} else {
|
||||
} else if (currentTab == 1) {
|
||||
loadFriendRequests();
|
||||
} else {
|
||||
loadBlockedList();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示好友操作对话框
|
||||
private void showFriendOptionsDialog(FriendItem friend, int position) {
|
||||
if (friend == null) return;
|
||||
|
||||
String[] options = {"删除好友", "拉黑"};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(friend.getName())
|
||||
.setItems(options, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
// 删除好友
|
||||
confirmDeleteFriend(friend, position);
|
||||
} else if (which == 1) {
|
||||
// 拉黑
|
||||
confirmBlockFriend(friend, position);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
// 显示黑名单操作对话框
|
||||
private void showBlockedOptionsDialog(FriendItem blocked, int position) {
|
||||
if (blocked == null) return;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(blocked.getName())
|
||||
.setMessage("确定要取消拉黑吗?")
|
||||
.setPositiveButton("确定", (dialog, which) -> unblockFriend(blocked, position))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// 确认删除好友
|
||||
private void confirmDeleteFriend(FriendItem friend, int position) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("删除好友")
|
||||
.setMessage("确定要删除好友 " + friend.getName() + " 吗?")
|
||||
.setPositiveButton("确定", (dialog, which) -> deleteFriend(friend, position))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// 确认拉黑好友
|
||||
private void confirmBlockFriend(FriendItem friend, int position) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("拉黑好友")
|
||||
.setMessage("拉黑后将删除好友关系,确定要拉黑 " + friend.getName() + " 吗?")
|
||||
.setPositiveButton("确定", (dialog, which) -> blockFriend(friend, position))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// 删除好友
|
||||
private void deleteFriend(FriendItem friend, int position) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) {
|
||||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/friends/" + friend.getId();
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.delete()
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "删除好友失败", e);
|
||||
runOnUiThread(() -> Toast.makeText(MyFriendsActivity.this, "删除失败,请重试", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
Toast.makeText(MyFriendsActivity.this, "已删除好友", Toast.LENGTH_SHORT).show();
|
||||
allFriends.remove(friend);
|
||||
friendsAdapter.submitList(new ArrayList<>(allFriends));
|
||||
updateEmptyState(allFriends);
|
||||
} else {
|
||||
String msg = json.optString("message", "删除失败");
|
||||
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 拉黑好友
|
||||
private void blockFriend(FriendItem friend, int position) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) {
|
||||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/friends/block/" + friend.getId();
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "拉黑好友失败", e);
|
||||
runOnUiThread(() -> Toast.makeText(MyFriendsActivity.this, "拉黑失败,请重试", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
Toast.makeText(MyFriendsActivity.this, "已拉黑", Toast.LENGTH_SHORT).show();
|
||||
allFriends.remove(friend);
|
||||
friendsAdapter.submitList(new ArrayList<>(allFriends));
|
||||
updateEmptyState(allFriends);
|
||||
} else {
|
||||
String msg = json.optString("message", "拉黑失败");
|
||||
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 取消拉黑
|
||||
private void unblockFriend(FriendItem blocked, int position) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) {
|
||||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/friends/unblock/" + blocked.getId();
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.post(RequestBody.create("", MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "取消拉黑失败", e);
|
||||
runOnUiThread(() -> Toast.makeText(MyFriendsActivity.this, "取消拉黑失败,请重试", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
Toast.makeText(MyFriendsActivity.this, "已取消拉黑", Toast.LENGTH_SHORT).show();
|
||||
allBlocked.remove(blocked);
|
||||
blockedAdapter.submitList(new ArrayList<>(allBlocked));
|
||||
updateBlockedEmptyState(allBlocked);
|
||||
} else {
|
||||
String msg = json.optString("message", "取消拉黑失败");
|
||||
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析响应失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载黑名单列表
|
||||
private void loadBlockedList() {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) {
|
||||
showEmptyState("登录后查看黑名单");
|
||||
return;
|
||||
}
|
||||
|
||||
binding.loadingProgress.setVisibility(View.VISIBLE);
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/friends/blocked?page=1&pageSize=100";
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "加载黑名单失败", e);
|
||||
runOnUiThread(() -> {
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
Toast.makeText(MyFriendsActivity.this, "加载失败,请重试", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
runOnUiThread(() -> {
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
try {
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optInt("code", -1) == 200) {
|
||||
JSONObject data = json.optJSONObject("data");
|
||||
JSONArray list = data != null ? data.optJSONArray("list") : null;
|
||||
parseBlockedList(list);
|
||||
} else {
|
||||
String msg = json.optString("message", "加载失败");
|
||||
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析黑名单失败", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void parseBlockedList(JSONArray list) {
|
||||
allBlocked.clear();
|
||||
if (list != null) {
|
||||
for (int i = 0; i < list.length(); i++) {
|
||||
try {
|
||||
JSONObject item = list.getJSONObject(i);
|
||||
String id = String.valueOf(item.opt("id"));
|
||||
String name = item.optString("name", "未知用户");
|
||||
String avatarUrl = item.optString("avatarUrl", "");
|
||||
String blockedTime = item.optString("blockedTime", "");
|
||||
String subtitle = "拉黑时间: " + blockedTime;
|
||||
allBlocked.add(new FriendItem(id, name, subtitle, false, avatarUrl));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析黑名单项失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
blockedAdapter.submitList(new ArrayList<>(allBlocked));
|
||||
updateBlockedEmptyState(allBlocked);
|
||||
}
|
||||
|
||||
private void updateBlockedEmptyState(List<FriendItem> blocked) {
|
||||
if (blocked == null || blocked.isEmpty()) {
|
||||
showEmptyState("暂无黑名单");
|
||||
} else {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
loadProfileFromPrefs();
|
||||
loadAndDisplayTags();
|
||||
loadProfileInfo();
|
||||
loadFollowStats(); // 加载关注统计
|
||||
setupEditableAreas();
|
||||
setupAvatarClick();
|
||||
setupNavigationClicks();
|
||||
|
|
@ -507,6 +508,7 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
loadProfileFromPrefs();
|
||||
loadAndDisplayTags();
|
||||
loadProfileInfo();
|
||||
loadFollowStats(); // 刷新关注统计
|
||||
loadWorks(); // 重新加载作品列表
|
||||
BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
|
||||
bottomNav.setSelectedItemId(R.id.nav_profile);
|
||||
|
|
@ -671,4 +673,44 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
String shareLink = ShareUtils.generateProfileShareLink(digits);
|
||||
ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载关注统计数据
|
||||
*/
|
||||
private void loadFollowStats() {
|
||||
com.example.livestreaming.net.ApiService apiService =
|
||||
com.example.livestreaming.net.RetrofitClient.getInstance(this).getApiService();
|
||||
retrofit2.Call<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> call =
|
||||
apiService.getFollowStats(null); // null表示查询当前用户
|
||||
|
||||
call.enqueue(new retrofit2.Callback<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> call,
|
||||
retrofit2.Response<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
java.util.Map<String, Object> stats = apiResponse.getData();
|
||||
|
||||
// 更新关注数
|
||||
Object followingCount = stats.get("followingCount");
|
||||
if (followingCount != null) {
|
||||
binding.following.setText(String.valueOf(followingCount) + "\n关注");
|
||||
}
|
||||
|
||||
// 更新粉丝数
|
||||
Object followersCount = stats.get("followersCount");
|
||||
if (followersCount != null) {
|
||||
binding.followers.setText(String.valueOf(followersCount) + "\n粉丝");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<com.example.livestreaming.net.ApiResponse<java.util.Map<String, Object>>> call, Throwable t) {
|
||||
// 忽略错误,使用默认显示
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,20 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.databinding.ActivityPublishWorkBinding;
|
||||
import com.example.livestreaming.databinding.ItemMediaPreviewBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.FileUploadResponse;
|
||||
import com.example.livestreaming.net.WorksRequest;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import retrofit2.Call;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -529,85 +543,311 @@ public class PublishWorkActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 发布作品
|
||||
// 接口路径: POST /api/works
|
||||
// 请求方法: POST
|
||||
// 请求头:
|
||||
// - Authorization: Bearer {token} (从AuthStore获取)
|
||||
// 请求参数(multipart/form-data):
|
||||
// - title: String (必填) - 作品标题
|
||||
// - description: String (可选) - 作品描述
|
||||
// - type: String (必填) - 作品类型 "IMAGE" 或 "VIDEO"
|
||||
// - cover: File (必填) - 封面图片文件
|
||||
// - video: File (可选) - 视频文件(当type为VIDEO时必填)
|
||||
// - images: File[] (可选) - 图片文件数组(当type为IMAGE时必填,最多9张)
|
||||
// 返回数据格式: ApiResponse<WorkItem>
|
||||
// WorkItem对象应包含:
|
||||
// - id: String - 作品ID
|
||||
// - title: String - 作品标题
|
||||
// - description: String - 作品描述
|
||||
// - coverUrl: String - 封面图片URL
|
||||
// - videoUrl: String - 视频URL(如果是视频作品)
|
||||
// - imageUrls: String[] - 图片URL数组(如果是图片作品)
|
||||
// - likeCount: int - 点赞数
|
||||
// - viewCount: int - 观看数
|
||||
// - publishTime: long - 发布时间戳
|
||||
// - type: String - 作品类型
|
||||
// 实现步骤:
|
||||
// 1. 显示上传进度(可选)
|
||||
// 2. 先上传封面图片到文件服务器,获取coverUrl
|
||||
// 3. 如果是视频作品,上传视频文件,获取videoUrl
|
||||
// 4. 如果是图片作品,上传所有图片文件,获取imageUrls数组
|
||||
// 5. 调用发布接口,传递title、description、type、coverUrl、videoUrl或imageUrls
|
||||
// 6. 发布成功后,刷新作品列表(调用ProfileActivity的loadWorks或发送广播通知)
|
||||
// 7. 关闭发布页面
|
||||
// 错误处理:
|
||||
// - 上传失败:显示错误提示,允许重试
|
||||
// - 发布失败:显示错误提示,保留已上传的文件URL(可选,或删除已上传文件)
|
||||
// 注意:
|
||||
// - 上传文件前需要压缩图片(可选,节省流量)
|
||||
// - 视频文件较大,建议显示上传进度
|
||||
// - 需要处理网络异常、超时等情况
|
||||
|
||||
// 创建作品对象(临时,用于本地存储)
|
||||
WorkItem workItem = new WorkItem();
|
||||
workItem.setTitle(title);
|
||||
workItem.setDescription(description);
|
||||
workItem.setType(currentWorkType);
|
||||
|
||||
if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) {
|
||||
// 视频作品
|
||||
workItem.setVideoUri(selectedVideoUri);
|
||||
workItem.setCoverUri(selectedVideoUri);
|
||||
// 将URI转换为字符串保存(用于序列化)
|
||||
workItem.setVideoUrl(selectedVideoUri.toString());
|
||||
workItem.setCoverUrl(selectedVideoUri.toString());
|
||||
} else {
|
||||
// 图片作品
|
||||
workItem.setImageUris(new ArrayList<>(selectedMediaUris));
|
||||
// 将URI列表转换为字符串列表保存(用于序列化)
|
||||
List<String> imageUrlStrings = new ArrayList<>();
|
||||
for (Uri uri : selectedMediaUris) {
|
||||
if (uri != null) {
|
||||
imageUrlStrings.add(uri.toString());
|
||||
}
|
||||
}
|
||||
workItem.setImageUrls(imageUrlStrings);
|
||||
|
||||
// 使用选中的封面(如果已选择),否则使用第一张图片
|
||||
Uri coverUri = selectedCoverUri != null ? selectedCoverUri :
|
||||
(!selectedMediaUris.isEmpty() ? selectedMediaUris.get(0) : null);
|
||||
if (coverUri != null) {
|
||||
workItem.setCoverUri(coverUri);
|
||||
workItem.setCoverUrl(coverUri.toString());
|
||||
}
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.requireLogin(this, "发布作品需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 临时:保存到本地存储(等待后端接口)
|
||||
WorkManager.saveWork(this, workItem);
|
||||
// 显示加载对话框
|
||||
android.app.ProgressDialog progressDialog = new android.app.ProgressDialog(this);
|
||||
progressDialog.setMessage("正在发布作品...");
|
||||
progressDialog.setCancelable(false);
|
||||
progressDialog.show();
|
||||
|
||||
Toast.makeText(this, "发布成功", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
// 开始上传流程
|
||||
if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) {
|
||||
// 视频作品:先上传封面,再上传视频,最后发布
|
||||
uploadCoverImage(selectedCoverUri != null ? selectedCoverUri : selectedVideoUri,
|
||||
new UploadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String url) {
|
||||
String coverUrl = url;
|
||||
// 上传视频
|
||||
uploadVideo(selectedVideoUri, new UploadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String videoUrl) {
|
||||
// 发布作品
|
||||
publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
progressDialog.dismiss();
|
||||
Toast.makeText(PublishWorkActivity.this, "视频上传失败: " + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
progressDialog.dismiss();
|
||||
Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 图片作品:先上传封面,再上传所有图片,最后发布
|
||||
Uri coverUri = selectedCoverUri != null ? selectedCoverUri :
|
||||
(!selectedMediaUris.isEmpty() ? selectedMediaUris.get(0) : null);
|
||||
|
||||
uploadCoverImage(coverUri, new UploadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String coverUrl) {
|
||||
// 上传所有图片
|
||||
uploadImages(selectedMediaUris, new UploadImagesCallback() {
|
||||
@Override
|
||||
public void onSuccess(List<String> imageUrls) {
|
||||
// 发布作品
|
||||
publishWorkToServer(title, description, "IMAGE", coverUrl, null, imageUrls, progressDialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
progressDialog.dismiss();
|
||||
Toast.makeText(PublishWorkActivity.this, "图片上传失败: " + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
progressDialog.dismiss();
|
||||
Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传封面图片
|
||||
*/
|
||||
private void uploadCoverImage(Uri imageUri, UploadCallback callback) {
|
||||
if (imageUri == null) {
|
||||
callback.onFailure("封面图片不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File file = getFileFromUri(imageUri);
|
||||
if (file == null || !file.exists()) {
|
||||
callback.onFailure("文件不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
RequestBody requestFile = RequestBody.create(okhttp3.MediaType.parse("image/*"), file);
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("multipart", file.getName(), requestFile);
|
||||
RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works");
|
||||
RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0");
|
||||
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
Call<ApiResponse<FileUploadResponse>> call = apiService.uploadImage(body, model, pid);
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<FileUploadResponse>> call, retrofit2.Response<ApiResponse<FileUploadResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<FileUploadResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
callback.onSuccess(apiResponse.getData().getUrl());
|
||||
} else {
|
||||
callback.onFailure(apiResponse.getMsg() != null ? apiResponse.getMsg() : "上传失败");
|
||||
}
|
||||
} else {
|
||||
callback.onFailure("上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
|
||||
callback.onFailure(t.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频
|
||||
*/
|
||||
private void uploadVideo(Uri videoUri, UploadCallback callback) {
|
||||
if (videoUri == null) {
|
||||
callback.onFailure("视频文件不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File file = getFileFromUri(videoUri);
|
||||
if (file == null || !file.exists()) {
|
||||
callback.onFailure("文件不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
RequestBody requestFile = RequestBody.create(okhttp3.MediaType.parse("video/*"), file);
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("multipart", file.getName(), requestFile);
|
||||
RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works");
|
||||
RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0");
|
||||
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
Call<ApiResponse<FileUploadResponse>> call = apiService.uploadVideo(body, model, pid);
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<FileUploadResponse>> call, retrofit2.Response<ApiResponse<FileUploadResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<FileUploadResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
callback.onSuccess(apiResponse.getData().getUrl());
|
||||
} else {
|
||||
callback.onFailure(apiResponse.getMsg() != null ? apiResponse.getMsg() : "上传失败");
|
||||
}
|
||||
} else {
|
||||
callback.onFailure("上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
|
||||
callback.onFailure(t.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
callback.onFailure(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传多张图片
|
||||
*/
|
||||
private void uploadImages(List<Uri> imageUris, UploadImagesCallback callback) {
|
||||
if (imageUris == null || imageUris.isEmpty()) {
|
||||
callback.onFailure("图片列表不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> uploadedUrls = new ArrayList<>();
|
||||
uploadImageRecursive(imageUris, 0, uploadedUrls, callback);
|
||||
}
|
||||
|
||||
private void uploadImageRecursive(List<Uri> imageUris, int index, List<String> uploadedUrls, UploadImagesCallback callback) {
|
||||
if (index >= imageUris.size()) {
|
||||
callback.onSuccess(uploadedUrls);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadCoverImage(imageUris.get(index), new UploadCallback() {
|
||||
@Override
|
||||
public void onSuccess(String url) {
|
||||
uploadedUrls.add(url);
|
||||
uploadImageRecursive(imageUris, index + 1, uploadedUrls, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
callback.onFailure("第" + (index + 1) + "张图片上传失败: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布作品到服务器
|
||||
*/
|
||||
private void publishWorkToServer(String title, String description, String type,
|
||||
String coverUrl, String videoUrl, List<String> imageUrls,
|
||||
android.app.ProgressDialog progressDialog) {
|
||||
WorksRequest request = new WorksRequest();
|
||||
request.setTitle(title);
|
||||
request.setDescription(description);
|
||||
request.setType(type);
|
||||
request.setCoverUrl(coverUrl);
|
||||
request.setVideoUrl(videoUrl);
|
||||
request.setImageUrls(imageUrls);
|
||||
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
Call<ApiResponse<Long>> call = apiService.publishWork(request);
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<Long>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Long>> call, retrofit2.Response<ApiResponse<Long>> response) {
|
||||
progressDialog.dismiss();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Long> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200) {
|
||||
Toast.makeText(PublishWorkActivity.this, "发布成功", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} else {
|
||||
Toast.makeText(PublishWorkActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "发布失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(PublishWorkActivity.this, "发布失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Long>> call, Throwable t) {
|
||||
progressDialog.dismiss();
|
||||
Toast.makeText(PublishWorkActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URI获取文件
|
||||
*/
|
||||
private File getFileFromUri(Uri uri) {
|
||||
try {
|
||||
String path = null;
|
||||
|
||||
// 尝试从URI获取真实路径
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
Cursor cursor = getContentResolver().query(uri, new String[]{MediaStore.Images.Media.DATA}, null, null, null);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
path = cursor.getString(columnIndex);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
path = uri.getPath();
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
return new File(path);
|
||||
}
|
||||
|
||||
// 如果无法获取路径,复制文件到临时目录
|
||||
File tempFile = new File(getCacheDir(), "temp_" + System.currentTimeMillis());
|
||||
java.io.InputStream inputStream = getContentResolver().openInputStream(uri);
|
||||
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(tempFile);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
|
||||
return tempFile;
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("PublishWork", "获取文件失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传回调接口
|
||||
interface UploadCallback {
|
||||
void onSuccess(String url);
|
||||
void onFailure(String error);
|
||||
}
|
||||
|
||||
interface UploadImagesCallback {
|
||||
void onSuccess(List<String> urls);
|
||||
void onFailure(String error);
|
||||
}
|
||||
|
||||
// 媒体预览适配器
|
||||
|
|
|
|||
|
|
@ -27,7 +27,14 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
|||
import com.example.livestreaming.databinding.ActivityRoomDetailNewBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.example.livestreaming.net.CreateRechargeRequest;
|
||||
import com.example.livestreaming.net.CreateRechargeResponse;
|
||||
import com.example.livestreaming.net.OrderPayRequest;
|
||||
import com.example.livestreaming.net.OrderPayResultResponse;
|
||||
import com.example.livestreaming.net.RechargeOptionResponse;
|
||||
import com.example.livestreaming.net.RetrofitClient;
|
||||
import com.example.livestreaming.net.Room;
|
||||
import com.example.livestreaming.net.StreamConfig;
|
||||
import com.example.livestreaming.ShareUtils;
|
||||
|
|
@ -48,6 +55,10 @@ import okhttp3.WebSocketListener;
|
|||
import okio.ByteString;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import retrofit2.Call;
|
||||
|
|
@ -1072,24 +1083,13 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
RechargeAdapter rechargeAdapter = new RechargeAdapter();
|
||||
recyclerView.setAdapter(rechargeAdapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取充值选项列表
|
||||
// 接口路径: GET /api/recharge/options
|
||||
// 返回数据格式: ApiResponse<List<RechargeOption>>
|
||||
// RechargeOption对象应包含: id, coinAmount, price, discountLabel等字段
|
||||
List<RechargeOption> rechargeOptions = new ArrayList<>();
|
||||
rechargeOptions.add(new RechargeOption("1", 100, 10.0, "首充优惠"));
|
||||
rechargeOptions.add(new RechargeOption("2", 300, 30.0));
|
||||
rechargeOptions.add(new RechargeOption("3", 500, 50.0, "热门"));
|
||||
rechargeOptions.add(new RechargeOption("4", 1000, 100.0));
|
||||
rechargeOptions.add(new RechargeOption("5", 3000, 300.0, "最划算"));
|
||||
rechargeOptions.add(new RechargeOption("6", 5000, 500.0));
|
||||
|
||||
rechargeAdapter.setOptions(rechargeOptions);
|
||||
|
||||
// 显示当前余额
|
||||
android.widget.TextView currentBalance = dialogView.findViewById(R.id.currentBalance);
|
||||
currentBalance.setText(String.valueOf(userCoinBalance));
|
||||
|
||||
// 加载充值选项列表
|
||||
loadRechargeOptions(rechargeAdapter, dialogView);
|
||||
|
||||
// 取消按钮
|
||||
dialogView.findViewById(R.id.cancelButton).setOnClickListener(v -> rechargeDialog.dismiss());
|
||||
|
||||
|
|
@ -1101,39 +1101,237 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 发起充值请求
|
||||
// 接口路径: POST /api/recharge/create
|
||||
// 请求参数:
|
||||
// - optionId: 充值选项ID
|
||||
// - coinAmount: 金币数量
|
||||
// - price: 价格
|
||||
// 返回数据格式: ApiResponse<{orderId: string, paymentUrl: string}>
|
||||
// 返回支付订单ID和支付URL,跳转到支付页面或调用支付SDK
|
||||
|
||||
// TODO: 集成支付SDK(微信支付、支付宝等)
|
||||
// 1. 调用支付SDK发起支付
|
||||
// 2. 监听支付结果回调
|
||||
// 3. 支付成功后更新用户金币余额
|
||||
|
||||
// 模拟充值成功
|
||||
userCoinBalance += selectedOption.getCoinAmount();
|
||||
|
||||
// 更新礼物弹窗中的余额显示
|
||||
if (giftDialog != null && giftDialog.isShowing()) {
|
||||
View giftView = giftDialog.findViewById(R.id.coinBalance);
|
||||
if (giftView instanceof android.widget.TextView) {
|
||||
((android.widget.TextView) giftView).setText(String.valueOf(userCoinBalance));
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(this,
|
||||
String.format("充值成功!获得 %d 金币", selectedOption.getCoinAmount()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
rechargeDialog.dismiss();
|
||||
// 调用后端接口创建充值订单
|
||||
createRechargeOrder(selectedOption, rechargeDialog);
|
||||
});
|
||||
|
||||
rechargeDialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载充值选项列表
|
||||
*/
|
||||
private void loadRechargeOptions(RechargeAdapter adapter, View dialogView) {
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<List<RechargeOptionResponse>>> call = apiService.getRechargeOptions();
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<List<RechargeOptionResponse>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<RechargeOptionResponse>>> call,
|
||||
Response<ApiResponse<List<RechargeOptionResponse>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<List<RechargeOptionResponse>> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
List<RechargeOption> options = new ArrayList<>();
|
||||
for (RechargeOptionResponse optionResponse : apiResponse.getData()) {
|
||||
RechargeOption option = new RechargeOption(
|
||||
optionResponse.getId(),
|
||||
optionResponse.getCoinAmount().intValue(),
|
||||
optionResponse.getPrice().doubleValue(),
|
||||
optionResponse.getDiscountLabel()
|
||||
);
|
||||
options.add(option);
|
||||
}
|
||||
adapter.setOptions(options);
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"加载充值选项失败: " + apiResponse.getMsg(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
// 使用默认选项
|
||||
setDefaultRechargeOptions(adapter);
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"加载充值选项失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
// 使用默认选项
|
||||
setDefaultRechargeOptions(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<RechargeOptionResponse>>> call, Throwable t) {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
// 使用默认选项
|
||||
setDefaultRechargeOptions(adapter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认充值选项(当接口失败时使用)
|
||||
*/
|
||||
private void setDefaultRechargeOptions(RechargeAdapter adapter) {
|
||||
List<RechargeOption> rechargeOptions = new ArrayList<>();
|
||||
rechargeOptions.add(new RechargeOption("1", 100, 10.0, "首充优惠"));
|
||||
rechargeOptions.add(new RechargeOption("2", 300, 30.0));
|
||||
rechargeOptions.add(new RechargeOption("3", 500, 50.0, "热门"));
|
||||
rechargeOptions.add(new RechargeOption("4", 1000, 100.0));
|
||||
rechargeOptions.add(new RechargeOption("5", 3000, 300.0, "最划算"));
|
||||
rechargeOptions.add(new RechargeOption("6", 5000, 500.0));
|
||||
adapter.setOptions(rechargeOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建充值订单
|
||||
*/
|
||||
private void createRechargeOrder(RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
|
||||
CreateRechargeRequest request = new CreateRechargeRequest(
|
||||
Integer.parseInt(selectedOption.getId()),
|
||||
new java.math.BigDecimal(selectedOption.getCoinAmount()),
|
||||
new java.math.BigDecimal(selectedOption.getPrice())
|
||||
);
|
||||
|
||||
Call<ApiResponse<CreateRechargeResponse>> call = apiService.createRecharge(request);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<CreateRechargeResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<CreateRechargeResponse>> call,
|
||||
Response<ApiResponse<CreateRechargeResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<CreateRechargeResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
CreateRechargeResponse rechargeResponse = apiResponse.getData();
|
||||
String orderId = rechargeResponse.getOrderId();
|
||||
String paymentUrl = rechargeResponse.getPaymentUrl();
|
||||
|
||||
// 显示支付选择对话框
|
||||
showPaymentMethodDialog(orderId, selectedOption, rechargeDialog);
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"创建充值订单失败: " + apiResponse.getMsg(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"创建充值订单失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<CreateRechargeResponse>> call, Throwable t) {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示支付方式选择对话框
|
||||
*/
|
||||
private void showPaymentMethodDialog(String orderId, RechargeOption selectedOption,
|
||||
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
String[] paymentMethods = {"支付宝支付", "微信支付", "余额支付(模拟)"};
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle("选择支付方式")
|
||||
.setItems(paymentMethods, (dialog, which) -> {
|
||||
String payType;
|
||||
String payChannel;
|
||||
|
||||
switch (which) {
|
||||
case 0: // 支付宝
|
||||
payType = "alipay";
|
||||
payChannel = "appAliPay";
|
||||
break;
|
||||
case 1: // 微信
|
||||
payType = "weixin";
|
||||
payChannel = "weixinAppAndroid";
|
||||
break;
|
||||
case 2: // 余额支付(模拟)
|
||||
// 模拟充值成功
|
||||
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用支付接口
|
||||
processPayment(orderId, payType, payChannel, selectedOption, rechargeDialog);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付
|
||||
*/
|
||||
private void processPayment(String orderId, String payType, String payChannel,
|
||||
RechargeOption selectedOption, androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
|
||||
OrderPayRequest payRequest = new OrderPayRequest(orderId, payType, payChannel);
|
||||
Call<ApiResponse<OrderPayResultResponse>> call = apiService.payment(payRequest);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<OrderPayResultResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<OrderPayResultResponse>> call,
|
||||
Response<ApiResponse<OrderPayResultResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<OrderPayResultResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
OrderPayResultResponse payResult = apiResponse.getData();
|
||||
|
||||
// TODO: 集成支付SDK(微信支付、支付宝等)
|
||||
// 1. 根据payType调用对应的支付SDK
|
||||
// 2. 传入jsConfig参数发起支付
|
||||
// 3. 监听支付结果回调
|
||||
// 4. 支付成功后查询订单状态并更新余额
|
||||
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"支付接口调用成功,订单号: " + payResult.getOrderNo() +
|
||||
"\n请集成支付SDK完成实际支付",
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// 暂时模拟支付成功
|
||||
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"支付失败: " + apiResponse.getMsg(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"支付失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<OrderPayResultResponse>> call, Throwable t) {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟充值成功
|
||||
*/
|
||||
private void simulateRechargeSuccess(RechargeOption selectedOption,
|
||||
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
userCoinBalance += selectedOption.getCoinAmount();
|
||||
|
||||
// 更新礼物弹窗中的余额显示
|
||||
if (giftDialog != null && giftDialog.isShowing()) {
|
||||
View giftView = giftDialog.findViewById(R.id.coinBalance);
|
||||
if (giftView instanceof android.widget.TextView) {
|
||||
((android.widget.TextView) giftView).setText(String.valueOf(userCoinBalance));
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(this,
|
||||
String.format("充值成功!获得 %d 金币", selectedOption.getCoinAmount()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
rechargeDialog.dismiss();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,24 +5,40 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivitySearchBinding;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.HotSearchResponse;
|
||||
import com.example.livestreaming.net.PageResponse;
|
||||
import com.example.livestreaming.net.RetrofitClient;
|
||||
import com.example.livestreaming.net.Room;
|
||||
import com.example.livestreaming.net.SearchHistoryResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SearchActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "SearchActivity";
|
||||
private ActivitySearchBinding binding;
|
||||
private RoomsAdapter adapter;
|
||||
|
||||
private final List<Room> all = new ArrayList<>();
|
||||
private boolean isSearching = false;
|
||||
private String lastSearchKeyword = "";
|
||||
|
||||
private static final String EXTRA_SEARCH_QUERY = "search_query";
|
||||
|
||||
|
|
@ -50,11 +66,15 @@ public class SearchActivity extends AppCompatActivity {
|
|||
binding.backButton.setOnClickListener(v -> finish());
|
||||
binding.cancelBtn.setOnClickListener(v -> finish());
|
||||
|
||||
// 如果从Intent中获取到搜索关键词,自动填充到搜索框
|
||||
// 如果从Intent中获取到搜索关键词,自动填充到搜索框并执行搜索
|
||||
String searchQuery = getIntent().getStringExtra(EXTRA_SEARCH_QUERY);
|
||||
if (searchQuery != null && !searchQuery.trim().isEmpty()) {
|
||||
binding.searchInput.setText(searchQuery);
|
||||
binding.searchInput.setSelection(searchQuery.length());
|
||||
performSearch(searchQuery);
|
||||
} else {
|
||||
// 加载热门搜索
|
||||
loadHotSearch();
|
||||
}
|
||||
|
||||
binding.searchInput.requestFocus();
|
||||
|
|
@ -72,24 +92,25 @@ public class SearchActivity extends AppCompatActivity {
|
|||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
binding.resultsRecyclerView.setLayoutManager(glm);
|
||||
binding.resultsRecyclerView.setAdapter(adapter);
|
||||
|
||||
// TODO: 接入后端接口 - 搜索房间/主播
|
||||
// 接口路径: GET /api/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词(必填)
|
||||
// - type (可选): 搜索类型(room/user/all),默认all
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
|
||||
// Room对象应包含: id, title, streamerName, type, isLive, coverUrl等字段
|
||||
// User对象应包含: id, name, avatarUrl, bio, isLive等字段
|
||||
all.clear();
|
||||
all.addAll(buildDemoRooms(24));
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
}
|
||||
|
||||
private void setupInput() {
|
||||
binding.searchInput.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
|
||||
|
||||
// 监听搜索按钮点击
|
||||
binding.searchInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
String keyword = binding.searchInput.getText().toString().trim();
|
||||
if (!keyword.isEmpty()) {
|
||||
performSearch(keyword);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 实时搜索建议(可选,暂时注释掉以避免频繁请求)
|
||||
/*
|
||||
binding.searchInput.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
|
@ -97,13 +118,193 @@ public class SearchActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
applyFilter(s != null ? s.toString() : "");
|
||||
String keyword = s != null ? s.toString().trim() : "";
|
||||
if (!keyword.isEmpty() && keyword.length() >= 2) {
|
||||
loadSearchSuggestions(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
private void performSearch(String keyword) {
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyword = keyword.trim();
|
||||
|
||||
// 避免重复搜索
|
||||
if (keyword.equals(lastSearchKeyword) && isSearching) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastSearchKeyword = keyword;
|
||||
isSearching = true;
|
||||
|
||||
Log.d(TAG, "执行搜索: " + keyword);
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> call =
|
||||
apiService.searchLiveRooms(keyword, null, null, 1, 20);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
|
||||
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
|
||||
isSearching = false;
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<PageResponse<Map<String, Object>>> apiResponse = response.body();
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
PageResponse<Map<String, Object>> pageResponse = apiResponse.getData();
|
||||
List<Map<String, Object>> rooms = pageResponse.getList();
|
||||
|
||||
if (rooms != null && !rooms.isEmpty()) {
|
||||
List<Room> roomList = new ArrayList<>();
|
||||
for (Map<String, Object> roomData : rooms) {
|
||||
Room room = parseRoomFromMap(roomData);
|
||||
if (room != null) {
|
||||
roomList.add(room);
|
||||
}
|
||||
}
|
||||
|
||||
all.clear();
|
||||
all.addAll(roomList);
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
updateEmptyState(all);
|
||||
|
||||
Log.d(TAG, "搜索成功,找到 " + roomList.size() + " 个直播间");
|
||||
} else {
|
||||
all.clear();
|
||||
adapter.submitList(new ArrayList<>());
|
||||
updateEmptyState(all);
|
||||
Log.d(TAG, "搜索结果为空");
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(SearchActivity.this,
|
||||
"搜索失败: " + apiResponse.getMsg(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "搜索失败: " + apiResponse.getMsg());
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(SearchActivity.this,
|
||||
"搜索失败,请稍后重试",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "搜索请求失败: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
|
||||
isSearching = false;
|
||||
Toast.makeText(SearchActivity.this,
|
||||
"网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
Log.e(TAG, "搜索网络错误", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Map解析Room对象
|
||||
*/
|
||||
private Room parseRoomFromMap(Map<String, Object> data) {
|
||||
try {
|
||||
Object idObj = data.get("id");
|
||||
String id = idObj != null ? String.valueOf(idObj) : null;
|
||||
|
||||
String title = (String) data.get("title");
|
||||
String streamerName = (String) data.get("streamerName");
|
||||
|
||||
Object isLiveObj = data.get("isLive");
|
||||
boolean isLive = false;
|
||||
if (isLiveObj instanceof Boolean) {
|
||||
isLive = (Boolean) isLiveObj;
|
||||
} else if (isLiveObj instanceof Number) {
|
||||
isLive = ((Number) isLiveObj).intValue() == 1;
|
||||
}
|
||||
|
||||
String coverUrl = (String) data.get("coverUrl");
|
||||
|
||||
Room room = new Room(id, title, streamerName, isLive);
|
||||
if (coverUrl != null) {
|
||||
room.setCoverUrl(coverUrl);
|
||||
}
|
||||
|
||||
return room;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析Room数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载热门搜索
|
||||
*/
|
||||
private void loadHotSearch() {
|
||||
Log.d(TAG, "加载热门搜索");
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<List<HotSearchResponse>>> call = apiService.getHotSearch(2, 10); // 2-直播间
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<List<HotSearchResponse>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<HotSearchResponse>>> call,
|
||||
Response<ApiResponse<List<HotSearchResponse>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<List<HotSearchResponse>> apiResponse = response.body();
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
List<HotSearchResponse> hotSearchList = apiResponse.getData();
|
||||
Log.d(TAG, "热门搜索加载成功: " + hotSearchList.size() + " 条");
|
||||
// TODO: 可以在UI上显示热门搜索标签
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<HotSearchResponse>>> call, Throwable t) {
|
||||
Log.e(TAG, "加载热门搜索失败", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载搜索建议(可选功能)
|
||||
*/
|
||||
private void loadSearchSuggestions(String keyword) {
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<List<String>>> call = apiService.getSearchSuggestions(keyword, 2, 10); // 2-直播间
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<List<String>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<String>>> call,
|
||||
Response<ApiResponse<List<String>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<List<String>> apiResponse = response.body();
|
||||
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
List<String> suggestions = apiResponse.getData();
|
||||
Log.d(TAG, "搜索建议: " + suggestions.size() + " 条");
|
||||
// TODO: 可以在UI上显示搜索建议下拉列表
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<String>>> call, Throwable t) {
|
||||
Log.e(TAG, "加载搜索建议失败", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void applyFilter(String q) {
|
||||
|
|
@ -159,16 +360,4 @@ public class SearchActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Room> buildDemoRooms(int count) {
|
||||
List<Room> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String id = "search-" + i;
|
||||
String title = "热门直播间 " + (i + 1);
|
||||
String streamer = "主播" + (i + 1);
|
||||
boolean live = i % 5 != 0;
|
||||
list.add(new Room(id, title, streamer, live));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,19 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityUserProfileReadOnlyBinding;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.RetrofitClient;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
||||
|
||||
|
|
@ -27,7 +36,8 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
private static final String EXTRA_BIO = "extra_bio";
|
||||
private static final String EXTRA_AVATAR_RES = "extra_avatar_res";
|
||||
|
||||
private static final String PREFS_FRIENDS = "friends_prefs";
|
||||
private String currentUserId;
|
||||
private boolean isFollowing = false;
|
||||
|
||||
public static void start(Context context, String userId, String name, String location, String bio, int avatarRes) {
|
||||
Intent intent = new Intent(context, UserProfileReadOnlyActivity.class);
|
||||
|
|
@ -47,7 +57,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
String userId = getIntent().getStringExtra(EXTRA_USER_ID);
|
||||
currentUserId = getIntent().getStringExtra(EXTRA_USER_ID);
|
||||
String name = getIntent().getStringExtra(EXTRA_NAME);
|
||||
String location = getIntent().getStringExtra(EXTRA_LOCATION);
|
||||
String bio = getIntent().getStringExtra(EXTRA_BIO);
|
||||
|
|
@ -66,35 +76,165 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
setupTabsAndWorks();
|
||||
bindDemoStatsAndWorks(userId);
|
||||
bindDemoStatsAndWorks(currentUserId);
|
||||
|
||||
boolean isFriend = isFriend(userId);
|
||||
updateAddFriendButton(isFriend);
|
||||
// 检查关注状态
|
||||
checkFollowStatus();
|
||||
|
||||
// TODO: 接入后端接口 - 发送好友请求
|
||||
// 接口路径: POST /api/friends/request
|
||||
// 请求参数:
|
||||
// - targetUserId: 目标用户ID
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 发送成功后,更新按钮状态为"已发送"或"已添加"
|
||||
// 关注/取消关注按钮点击事件
|
||||
binding.addFriendButton.setOnClickListener(v -> {
|
||||
if (TextUtils.isEmpty(userId)) {
|
||||
if (TextUtils.isEmpty(currentUserId)) {
|
||||
Toast.makeText(this, "用户信息缺失", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean now = isFriend(userId);
|
||||
if (!now) {
|
||||
setFriend(userId, true);
|
||||
updateAddFriendButton(true);
|
||||
Toast.makeText(this, "已发送好友请求", Toast.LENGTH_SHORT).show();
|
||||
if (isFollowing) {
|
||||
unfollowUser();
|
||||
} else {
|
||||
Toast.makeText(this, "已添加", Toast.LENGTH_SHORT).show();
|
||||
followUser();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void checkFollowStatus() {
|
||||
if (TextUtils.isEmpty(currentUserId)) return;
|
||||
|
||||
try {
|
||||
int userId = Integer.parseInt(currentUserId);
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<Map<String, Object>>> call = apiService.checkFollowStatus(userId);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
||||
Response<ApiResponse<Map<String, Object>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
Map<String, Object> data = apiResponse.getData();
|
||||
Boolean following = (Boolean) data.get("isFollowing");
|
||||
isFollowing = following != null && following;
|
||||
updateFollowButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
||||
// 忽略错误,使用默认状态
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
private void followUser() {
|
||||
if (TextUtils.isEmpty(currentUserId)) return;
|
||||
|
||||
try {
|
||||
int userId = Integer.parseInt(currentUserId);
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("userId", userId);
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<Map<String, Object>>> call = apiService.followUser(requestBody);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
||||
Response<ApiResponse<Map<String, Object>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200) {
|
||||
isFollowing = true;
|
||||
updateFollowButton();
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "关注成功", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "关注失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void unfollowUser() {
|
||||
if (TextUtils.isEmpty(currentUserId)) return;
|
||||
|
||||
try {
|
||||
int userId = Integer.parseInt(currentUserId);
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("userId", userId);
|
||||
|
||||
ApiService apiService = RetrofitClient.getInstance(this).getApiService();
|
||||
Call<ApiResponse<Map<String, Object>>> call = apiService.unfollowUser(requestBody);
|
||||
|
||||
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
||||
Response<ApiResponse<Map<String, Object>>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Map<String, Object>> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200) {
|
||||
isFollowing = false;
|
||||
updateFollowButton();
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "取消关注成功", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "取消关注失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
||||
Toast.makeText(UserProfileReadOnlyActivity.this, "网络错误: " + t.getMessage(),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFollowButton() {
|
||||
if (binding == null) return;
|
||||
if (isFollowing) {
|
||||
binding.addFriendButton.setText("已关注");
|
||||
binding.addFriendButton.setAlpha(0.7f);
|
||||
} else {
|
||||
binding.addFriendButton.setText("关注");
|
||||
binding.addFriendButton.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFollowButton() {
|
||||
if (binding == null) return;
|
||||
if (isFollowing) {
|
||||
binding.addFriendButton.setText("已关注");
|
||||
binding.addFriendButton.setAlpha(0.7f);
|
||||
} else {
|
||||
binding.addFriendButton.setText("关注");
|
||||
binding.addFriendButton.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupTabsAndWorks() {
|
||||
if (binding == null) return;
|
||||
|
||||
|
|
@ -132,27 +272,6 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showTab(int index) {
|
||||
// TODO: 接入后端接口 - 获取其他用户的作品列表
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// TODO: 接入后端接口 - 获取其他用户的收藏列表
|
||||
// 接口路径: GET /api/users/{userId}/favorites
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// TODO: 接入后端接口 - 获取其他用户赞过的作品列表
|
||||
// 接口路径: GET /api/users/{userId}/liked
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
if (binding == null) return;
|
||||
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
||||
binding.worksRecycler.setVisibility(index == 0 ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
|
|
@ -161,21 +280,6 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void bindDemoStatsAndWorks(String userId) {
|
||||
// TODO: 接入后端接口 - 获取其他用户资料和统计数据
|
||||
// 接口路径: GET /api/users/{userId}/profile
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<UserProfile>
|
||||
// UserProfile对象应包含: id, name, avatarUrl, bio, location, worksCount, followingCount,
|
||||
// followersCount, likesCount等字段
|
||||
// TODO: 接入后端接口 - 获取用户作品列表
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// WorkItem对象应包含: id, coverUrl, title, likeCount, viewCount等字段
|
||||
if (binding == null) return;
|
||||
|
||||
String seed = !TextUtils.isEmpty(userId) ? userId : "demo";
|
||||
|
|
@ -191,8 +295,7 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
binding.statFollowersValue.setText(String.valueOf(followers));
|
||||
binding.statLikesValue.setText(String.valueOf(likes));
|
||||
|
||||
// TODO: 接入后端接口 - 获取用户作品列表
|
||||
// 目前使用演示数据,创建临时的WorkItem列表
|
||||
// 创建演示作品列表
|
||||
List<WorkItem> works = new ArrayList<>();
|
||||
int[] pool = new int[] {
|
||||
R.drawable.wish_tree_checker_backup,
|
||||
|
|
@ -208,8 +311,6 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
work.setType(WorkItem.WorkType.IMAGE);
|
||||
work.setLikeCount((h + i) % 100);
|
||||
work.setViewCount((h + i) % 500);
|
||||
// 注意:这里使用drawable资源ID作为临时方案
|
||||
// 实际应该使用从服务器获取的WorkItem对象
|
||||
works.add(work);
|
||||
}
|
||||
if (worksAdapter != null) {
|
||||
|
|
@ -221,25 +322,4 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
worksAdapter.submitList(works);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFriend(String userId) {
|
||||
if (TextUtils.isEmpty(userId)) return false;
|
||||
return getSharedPreferences(PREFS_FRIENDS, MODE_PRIVATE).getBoolean(userId, false);
|
||||
}
|
||||
|
||||
private void setFriend(String userId, boolean value) {
|
||||
if (TextUtils.isEmpty(userId)) return;
|
||||
getSharedPreferences(PREFS_FRIENDS, MODE_PRIVATE).edit().putBoolean(userId, value).apply();
|
||||
}
|
||||
|
||||
private void updateAddFriendButton(boolean isFriend) {
|
||||
if (binding == null) return;
|
||||
if (isFriend) {
|
||||
binding.addFriendButton.setText("已添加");
|
||||
binding.addFriendButton.setAlpha(0.7f);
|
||||
} else {
|
||||
binding.addFriendButton.setText("加好友");
|
||||
binding.addFriendButton.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,19 @@ import androidx.viewpager2.widget.ViewPager2;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.databinding.ActivityWorkDetailBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.example.livestreaming.net.WorksResponse;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
|
||||
public class WorkDetailActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityWorkDetailBinding binding;
|
||||
|
|
@ -57,79 +64,8 @@ public class WorkDetailActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TODO: 接入后端接口 - 获取作品详情
|
||||
// ============================================
|
||||
// 接口路径: GET /api/works/{workId}
|
||||
// 请求方法: GET
|
||||
// 请求头:
|
||||
// - Authorization: Bearer {token} (可选,登录用户可获取更多信息)
|
||||
// 路径参数:
|
||||
// - workId: String (必填) - 作品ID
|
||||
// 返回数据格式: ApiResponse<WorkDetailResponse>
|
||||
//
|
||||
// 后端需要返回的数据结构 (WorkDetailResponse):
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "success",
|
||||
// "data": {
|
||||
// "id": "String - 作品ID",
|
||||
// "title": "String - 作品标题",
|
||||
// "description": "String - 作品描述(可选)",
|
||||
// "type": "String - 作品类型: IMAGE 或 VIDEO",
|
||||
// "coverUrl": "String - 封面图片URL",
|
||||
// "videoUrl": "String - 视频URL(视频作品时必填)",
|
||||
// "imageUrls": ["String"] - 图片URL数组(图片作品时必填,最多9张)",
|
||||
// "likeCount": "int - 点赞数",
|
||||
// "favoriteCount": "int - 收藏数",
|
||||
// "commentCount": "int - 评论数",
|
||||
// "viewCount": "int - 观看数",
|
||||
// "publishTime": "long - 发布时间戳(毫秒)",
|
||||
// "userId": "String - 作者用户ID",
|
||||
// "userName": "String - 作者用户名",
|
||||
// "userAvatar": "String - 作者头像URL",
|
||||
// "isLiked": "boolean - 当前用户是否已点赞(需要登录)",
|
||||
// "isFavorited": "boolean - 当前用户是否已收藏(需要登录)",
|
||||
// "isOwner": "boolean - 是否是当前用户的作品(需要登录)"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 前端需要传入的参数:
|
||||
// - workId: String (从Intent中获取)
|
||||
// - token: String (可选,从AuthStore获取,如果用户已登录)
|
||||
//
|
||||
// 实现步骤:
|
||||
// 1. 显示加载状态(ProgressBar或LoadingDialog)
|
||||
// 2. 从AuthStore获取token(如果用户已登录)
|
||||
// 3. 调用接口 GET /api/works/{workId},携带token(如果有)
|
||||
// 4. 解析返回数据,更新workItem对象
|
||||
// 5. 更新UI显示作品信息(标题、观看数、点赞数、收藏数、评论数)
|
||||
// 6. 根据isLiked和isFavorited更新按钮状态
|
||||
// 7. 根据isOwner决定是否显示编辑/删除按钮
|
||||
// 8. 加载媒体内容(图片或视频)
|
||||
// 9. 处理错误情况:
|
||||
// - 401: 未登录(可选,不影响查看作品)
|
||||
// - 404: 作品不存在,显示错误提示并关闭页面
|
||||
// - 500: 服务器错误,显示错误提示
|
||||
// - 网络错误: 显示网络错误提示,允许重试
|
||||
//
|
||||
// 注意:
|
||||
// - 调用此接口可能会增加作品的观看数(viewCount)
|
||||
// - 如果用户未登录,isLiked、isFavorited、isOwner字段可能为false或null
|
||||
// - 图片/视频URL应该是完整的可访问URL(支持HTTP/HTTPS)
|
||||
|
||||
// 临时:从本地存储加载(等待后端接口)
|
||||
workItem = WorkManager.getWorkById(this, workId);
|
||||
if (workItem == null) {
|
||||
Toast.makeText(this, "作品不存在", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setupToolbar();
|
||||
setupContent();
|
||||
setupActionButtons();
|
||||
setupActionButton();
|
||||
// 加载作品详情
|
||||
loadWorkDetail(workId);
|
||||
}
|
||||
|
||||
private void setupToolbar() {
|
||||
|
|
@ -500,113 +436,136 @@ public class WorkDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void toggleLike() {
|
||||
// ============================================
|
||||
// TODO: 接入后端接口 - 点赞/取消点赞
|
||||
// ============================================
|
||||
// 接口路径: POST /api/works/{workId}/like (点赞) 或 DELETE /api/works/{workId}/like (取消点赞)
|
||||
// 请求方法: POST 或 DELETE
|
||||
// 请求头:
|
||||
// - Authorization: Bearer {token} (必填,需要登录)
|
||||
// 路径参数:
|
||||
// - workId: String (必填) - 作品ID
|
||||
// 返回数据格式: ApiResponse<LikeResponse>
|
||||
//
|
||||
// 后端需要返回的数据结构 (LikeResponse):
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "success",
|
||||
// "data": {
|
||||
// "isLiked": "boolean - 当前点赞状态",
|
||||
// "likeCount": "int - 更新后的点赞数"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 前端需要传入的参数:
|
||||
// - workId: String (从workItem.getId()获取)
|
||||
// - token: String (必填,从AuthStore获取)
|
||||
//
|
||||
// 实现步骤:
|
||||
// 1. 检查用户是否登录,未登录则提示需要登录
|
||||
// 2. 根据当前isLiked状态决定调用点赞或取消点赞接口
|
||||
// 3. 如果isLiked为false,调用 POST /api/works/{workId}/like
|
||||
// 4. 如果isLiked为true,调用 DELETE /api/works/{workId}/like
|
||||
// 5. 解析返回数据,更新isLiked和likeCount
|
||||
// 6. 更新UI(按钮颜色和点赞数)
|
||||
// 7. 显示成功提示
|
||||
// 8. 处理错误情况:
|
||||
// - 401: 未登录,跳转到登录页
|
||||
// - 404: 作品不存在,显示错误提示
|
||||
// - 500: 服务器错误,显示错误提示,恢复原状态
|
||||
// - 网络错误: 显示网络错误提示,恢复原状态
|
||||
//
|
||||
// 注意:
|
||||
// - 需要先检查登录状态,未登录用户不能点赞
|
||||
// - 操作失败时需要恢复UI状态(乐观更新需要回滚)
|
||||
|
||||
// 临时实现(等待后端接口)
|
||||
isLiked = !isLiked;
|
||||
if (isLiked) {
|
||||
workItem.setLikeCount(workItem.getLikeCount() + 1);
|
||||
Toast.makeText(this, "已点赞", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
workItem.setLikeCount(Math.max(0, workItem.getLikeCount() - 1));
|
||||
Toast.makeText(this, "已取消点赞", Toast.LENGTH_SHORT).show();
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.requireLogin(this, "点赞需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long worksId = Long.parseLong(workItem.getId());
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
|
||||
Call<ApiResponse<Boolean>> call;
|
||||
if (isLiked) {
|
||||
// 取消点赞
|
||||
call = apiService.unlikeWork(worksId);
|
||||
} else {
|
||||
// 点赞
|
||||
call = apiService.likeWork(worksId);
|
||||
}
|
||||
|
||||
// 乐观更新UI
|
||||
boolean oldLiked = isLiked;
|
||||
int oldCount = workItem.getLikeCount();
|
||||
isLiked = !isLiked;
|
||||
if (isLiked) {
|
||||
workItem.setLikeCount(workItem.getLikeCount() + 1);
|
||||
} else {
|
||||
workItem.setLikeCount(Math.max(0, workItem.getLikeCount() - 1));
|
||||
}
|
||||
updateLikeButton();
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Boolean> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200) {
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
isLiked ? "已点赞" : "已取消点赞",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// 恢复原状态
|
||||
isLiked = oldLiked;
|
||||
workItem.setLikeCount(oldCount);
|
||||
updateLikeButton();
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "操作失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
// 恢复原状态
|
||||
isLiked = oldLiked;
|
||||
workItem.setLikeCount(oldCount);
|
||||
updateLikeButton();
|
||||
Toast.makeText(WorkDetailActivity.this, "操作失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Boolean>> call, Throwable t) {
|
||||
// 恢复原状态
|
||||
isLiked = oldLiked;
|
||||
workItem.setLikeCount(oldCount);
|
||||
updateLikeButton();
|
||||
Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "作品ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
updateLikeButton();
|
||||
}
|
||||
|
||||
private void toggleFavorite() {
|
||||
// ============================================
|
||||
// TODO: 接入后端接口 - 收藏/取消收藏
|
||||
// ============================================
|
||||
// 接口路径: POST /api/works/{workId}/favorite (收藏) 或 DELETE /api/works/{workId}/favorite (取消收藏)
|
||||
// 请求方法: POST 或 DELETE
|
||||
// 请求头:
|
||||
// - Authorization: Bearer {token} (必填,需要登录)
|
||||
// 路径参数:
|
||||
// - workId: String (必填) - 作品ID
|
||||
// 返回数据格式: ApiResponse<FavoriteResponse>
|
||||
//
|
||||
// 后端需要返回的数据结构 (FavoriteResponse):
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "success",
|
||||
// "data": {
|
||||
// "isFavorited": "boolean - 当前收藏状态",
|
||||
// "favoriteCount": "int - 更新后的收藏数"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 前端需要传入的参数:
|
||||
// - workId: String (从workItem.getId()获取)
|
||||
// - token: String (必填,从AuthStore获取)
|
||||
//
|
||||
// 实现步骤:
|
||||
// 1. 检查用户是否登录,未登录则提示需要登录
|
||||
// 2. 根据当前isFavorited状态决定调用收藏或取消收藏接口
|
||||
// 3. 如果isFavorited为false,调用 POST /api/works/{workId}/favorite
|
||||
// 4. 如果isFavorited为true,调用 DELETE /api/works/{workId}/favorite
|
||||
// 5. 解析返回数据,更新isFavorited和favoriteCount
|
||||
// 6. 更新UI(按钮颜色和收藏数)
|
||||
// 7. 显示成功提示
|
||||
// 8. 处理错误情况:
|
||||
// - 401: 未登录,跳转到登录页
|
||||
// - 404: 作品不存在,显示错误提示
|
||||
// - 500: 服务器错误,显示错误提示,恢复原状态
|
||||
// - 网络错误: 显示网络错误提示,恢复原状态
|
||||
//
|
||||
// 注意:
|
||||
// - 需要先检查登录状态,未登录用户不能收藏
|
||||
// - 操作失败时需要恢复UI状态(乐观更新需要回滚)
|
||||
|
||||
// 临时实现(等待后端接口)
|
||||
isFavorited = !isFavorited;
|
||||
if (isFavorited) {
|
||||
Toast.makeText(this, "已收藏", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "已取消收藏", Toast.LENGTH_SHORT).show();
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.requireLogin(this, "收藏需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long worksId = Long.parseLong(workItem.getId());
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
|
||||
Call<ApiResponse<Boolean>> call;
|
||||
if (isFavorited) {
|
||||
// 取消收藏
|
||||
call = apiService.uncollectWork(worksId);
|
||||
} else {
|
||||
// 收藏
|
||||
call = apiService.collectWork(worksId);
|
||||
}
|
||||
|
||||
// 乐观更新UI
|
||||
boolean oldFavorited = isFavorited;
|
||||
isFavorited = !isFavorited;
|
||||
updateFavoriteButton();
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Boolean> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200) {
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
isFavorited ? "已收藏" : "已取消收藏",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// 恢复原状态
|
||||
isFavorited = oldFavorited;
|
||||
updateFavoriteButton();
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "操作失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
// 恢复原状态
|
||||
isFavorited = oldFavorited;
|
||||
updateFavoriteButton();
|
||||
Toast.makeText(WorkDetailActivity.this, "操作失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Boolean>> call, Throwable t) {
|
||||
// 恢复原状态
|
||||
isFavorited = oldFavorited;
|
||||
updateFavoriteButton();
|
||||
Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "作品ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
updateFavoriteButton();
|
||||
}
|
||||
|
||||
private void setupActionButton() {
|
||||
|
|
@ -733,58 +692,41 @@ public class WorkDetailActivity extends AppCompatActivity {
|
|||
.setTitle("删除作品")
|
||||
.setMessage("确定要删除这个作品吗?")
|
||||
.setPositiveButton("删除", (dialog, which) -> {
|
||||
// ============================================
|
||||
// TODO: 接入后端接口 - 删除作品
|
||||
// ============================================
|
||||
// 接口路径: DELETE /api/works/{workId}
|
||||
// 请求方法: DELETE
|
||||
// 请求头:
|
||||
// - Authorization: Bearer {token} (必填,需要登录)
|
||||
// 路径参数:
|
||||
// - workId: String (必填) - 作品ID
|
||||
// 返回数据格式: ApiResponse<Object>
|
||||
//
|
||||
// 后端需要返回的数据结构:
|
||||
// {
|
||||
// "code": 200,
|
||||
// "message": "删除成功",
|
||||
// "data": null
|
||||
// }
|
||||
//
|
||||
// 前端需要传入的参数:
|
||||
// - workId: String (从workItem.getId()获取)
|
||||
// - token: String (必填,从AuthStore获取)
|
||||
//
|
||||
// 实现步骤:
|
||||
// 1. 显示删除确认对话框(已实现)
|
||||
// 2. 用户确认后,显示加载状态
|
||||
// 3. 从AuthStore获取token
|
||||
// 4. 调用 DELETE /api/works/{workId},携带token
|
||||
// 5. 删除成功:
|
||||
// - 显示成功提示
|
||||
// - 关闭当前页面
|
||||
// - 发送广播或回调通知作品列表页面刷新(可选)
|
||||
// 6. 删除失败:显示错误提示
|
||||
//
|
||||
// 错误处理:
|
||||
// - 401: 未登录,跳转到登录页
|
||||
// - 403: 无权限(不是作品作者),显示错误提示"无权限删除此作品"
|
||||
// - 404: 作品不存在,显示错误提示"作品不存在",关闭页面
|
||||
// - 500: 服务器错误,显示错误提示"删除失败,请稍后重试"
|
||||
// - 网络错误: 显示网络错误提示,允许重试
|
||||
//
|
||||
// 注意:
|
||||
// - 删除后,服务器会删除关联的图片/视频文件
|
||||
// - 删除操作不可恢复,需要确认对话框
|
||||
// - 只有作品作者才能删除
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.requireLogin(this, "删除作品需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 临时:从本地存储删除(等待后端接口)
|
||||
boolean deleted = WorkManager.deleteWork(this, workItem.getId());
|
||||
if (deleted) {
|
||||
Toast.makeText(this, "删除成功", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} else {
|
||||
Toast.makeText(this, "删除失败", Toast.LENGTH_SHORT).show();
|
||||
try {
|
||||
long worksId = Long.parseLong(workItem.getId());
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
Call<ApiResponse<Boolean>> call = apiService.deleteWork(worksId);
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<Boolean> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && Boolean.TRUE.equals(apiResponse.getData())) {
|
||||
Toast.makeText(WorkDetailActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} else {
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "删除失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(WorkDetailActivity.this, "删除失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<Boolean>> call, Throwable t) {
|
||||
Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "作品ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
|
|
@ -837,5 +779,102 @@ public class WorkDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载作品详情
|
||||
*/
|
||||
private void loadWorkDetail(String workId) {
|
||||
try {
|
||||
long worksId = Long.parseLong(workId);
|
||||
|
||||
ApiService apiService = ApiClient.getApiService(this);
|
||||
Call<ApiResponse<WorksResponse>> call = apiService.getWorkDetail(worksId);
|
||||
|
||||
call.enqueue(new retrofit2.Callback<ApiResponse<WorksResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<WorksResponse>> call, retrofit2.Response<ApiResponse<WorksResponse>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse<WorksResponse> apiResponse = response.body();
|
||||
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
WorksResponse worksResponse = apiResponse.getData();
|
||||
|
||||
// 转换为WorkItem对象
|
||||
workItem = convertToWorkItem(worksResponse);
|
||||
|
||||
// 更新状态
|
||||
isLiked = worksResponse.getIsLiked();
|
||||
isFavorited = worksResponse.getIsCollected();
|
||||
commentCount = worksResponse.getCommentCount();
|
||||
|
||||
// 设置UI
|
||||
setupToolbar();
|
||||
setupContent();
|
||||
setupActionButtons();
|
||||
setupActionButton();
|
||||
} else {
|
||||
Toast.makeText(WorkDetailActivity.this,
|
||||
apiResponse.getMsg() != null ? apiResponse.getMsg() : "获取作品详情失败",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(WorkDetailActivity.this, "获取作品详情失败", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<WorksResponse>> call, Throwable t) {
|
||||
Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "作品ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将WorksResponse转换为WorkItem
|
||||
*/
|
||||
private WorkItem convertToWorkItem(WorksResponse response) {
|
||||
WorkItem item = new WorkItem();
|
||||
item.setId(String.valueOf(response.getId()));
|
||||
item.setTitle(response.getTitle());
|
||||
item.setDescription(response.getDescription());
|
||||
item.setLikeCount(response.getLikeCount());
|
||||
item.setViewCount(response.getViewCount());
|
||||
item.setPublishTime(response.getPublishTime());
|
||||
|
||||
// 设置作品类型
|
||||
if ("VIDEO".equals(response.getType())) {
|
||||
item.setType(WorkItem.WorkType.VIDEO);
|
||||
item.setVideoUrl(response.getVideoUrl());
|
||||
if (!TextUtils.isEmpty(response.getVideoUrl())) {
|
||||
item.setVideoUri(Uri.parse(response.getVideoUrl()));
|
||||
}
|
||||
} else {
|
||||
item.setType(WorkItem.WorkType.IMAGE);
|
||||
item.setImageUrls(response.getImageUrls());
|
||||
if (response.getImageUrls() != null && !response.getImageUrls().isEmpty()) {
|
||||
List<Uri> imageUris = new ArrayList<>();
|
||||
for (String url : response.getImageUrls()) {
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
imageUris.add(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
item.setImageUris(imageUris);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置封面
|
||||
item.setCoverUrl(response.getCoverUrl());
|
||||
if (!TextUtils.isEmpty(response.getCoverUrl())) {
|
||||
item.setCoverUri(Uri.parse(response.getCoverUrl()));
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,17 @@ public interface ApiService {
|
|||
@DELETE("api/front/friends/{friendId}")
|
||||
Call<ApiResponse<Boolean>> deleteFriend(@Path("friendId") int friendId);
|
||||
|
||||
@POST("api/front/friends/block/{friendId}")
|
||||
Call<ApiResponse<Boolean>> blockFriend(@Path("friendId") int friendId);
|
||||
|
||||
@POST("api/front/friends/unblock/{friendId}")
|
||||
Call<ApiResponse<Boolean>> unblockFriend(@Path("friendId") int friendId);
|
||||
|
||||
@GET("api/front/friends/blocked")
|
||||
Call<ApiResponse<PageResponse<FriendResponse>>> getBlockedList(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/users/search")
|
||||
Call<ApiResponse<PageResponse<SearchUserResponse>>> searchUsers(
|
||||
@Query("keyword") String keyword,
|
||||
|
|
@ -160,6 +171,13 @@ public interface ApiService {
|
|||
@Part("model") RequestBody model,
|
||||
@Part("pid") RequestBody pid);
|
||||
|
||||
@Multipart
|
||||
@POST("api/front/upload/work/video")
|
||||
Call<ApiResponse<FileUploadResponse>> uploadVideo(
|
||||
@Part MultipartBody.Part file,
|
||||
@Part("model") RequestBody model,
|
||||
@Part("pid") RequestBody pid);
|
||||
|
||||
// ==================== 在线状态 ====================
|
||||
|
||||
@GET("api/front/online/status/{userId}")
|
||||
|
|
@ -189,4 +207,157 @@ public interface ApiService {
|
|||
|
||||
@DELETE("api/front/online/offline/messages/{userId}")
|
||||
Call<ApiResponse<String>> clearOfflineMessages(@Path("userId") int userId);
|
||||
|
||||
// ==================== 消息表情回应 ====================
|
||||
|
||||
@POST("api/front/messages/reactions/add")
|
||||
Call<ApiResponse<Boolean>> addMessageReaction(@Body Map<String, Object> body);
|
||||
|
||||
@DELETE("api/front/messages/reactions/remove")
|
||||
Call<ApiResponse<Boolean>> removeMessageReaction(@Body Map<String, Object> body);
|
||||
|
||||
@GET("api/front/messages/{messageId}/reactions")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getMessageReactions(@Path("messageId") String messageId);
|
||||
|
||||
@GET("api/front/messages/{messageId}/reactions/users")
|
||||
Call<ApiResponse<List<Map<String, Object>>>> getReactionUsers(
|
||||
@Path("messageId") String messageId,
|
||||
@Query("emoji") String emoji);
|
||||
|
||||
// ==================== 关注功能 ====================
|
||||
|
||||
@POST("api/front/follow/follow")
|
||||
Call<ApiResponse<Map<String, Object>>> followUser(@Body Map<String, Object> body);
|
||||
|
||||
@POST("api/front/follow/unfollow")
|
||||
Call<ApiResponse<Map<String, Object>>> unfollowUser(@Body Map<String, Object> body);
|
||||
|
||||
@GET("api/front/follow/status/{userId}")
|
||||
Call<ApiResponse<Map<String, Object>>> checkFollowStatus(@Path("userId") int userId);
|
||||
|
||||
@POST("api/front/follow/status/batch")
|
||||
Call<ApiResponse<Map<String, Object>>> batchCheckFollowStatus(@Body Map<String, Object> body);
|
||||
|
||||
@GET("api/front/follow/following")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> getFollowingList(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/follow/followers")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> getFollowersList(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/follow/stats")
|
||||
Call<ApiResponse<Map<String, Object>>> getFollowStats(@Query("userId") Integer userId);
|
||||
|
||||
// ==================== 作品管理 ====================
|
||||
|
||||
@POST("api/front/works/publish")
|
||||
Call<ApiResponse<Long>> publishWork(@Body WorksRequest body);
|
||||
|
||||
@POST("api/front/works/update")
|
||||
Call<ApiResponse<Boolean>> updateWork(@Body WorksRequest body);
|
||||
|
||||
@POST("api/front/works/delete/{worksId}")
|
||||
Call<ApiResponse<Boolean>> deleteWork(@Path("worksId") long worksId);
|
||||
|
||||
@GET("api/front/works/detail/{worksId}")
|
||||
Call<ApiResponse<WorksResponse>> getWorkDetail(@Path("worksId") long worksId);
|
||||
|
||||
@POST("api/front/works/search")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> searchWorks(@Body WorksSearchRequest body);
|
||||
|
||||
@GET("api/front/works/user/{userId}")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getUserWorks(
|
||||
@Path("userId") int userId,
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@POST("api/front/works/like/{worksId}")
|
||||
Call<ApiResponse<Boolean>> likeWork(@Path("worksId") long worksId);
|
||||
|
||||
@POST("api/front/works/unlike/{worksId}")
|
||||
Call<ApiResponse<Boolean>> unlikeWork(@Path("worksId") long worksId);
|
||||
|
||||
@POST("api/front/works/collect/{worksId}")
|
||||
Call<ApiResponse<Boolean>> collectWork(@Path("worksId") long worksId);
|
||||
|
||||
@POST("api/front/works/uncollect/{worksId}")
|
||||
Call<ApiResponse<Boolean>> uncollectWork(@Path("worksId") long worksId);
|
||||
|
||||
@GET("api/front/works/my/liked")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getMyLikedWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/works/my/collected")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getMyCollectedWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@POST("api/front/works/share/{worksId}")
|
||||
Call<ApiResponse<Boolean>> shareWork(@Path("worksId") long worksId);
|
||||
|
||||
// ==================== 搜索功能 ====================
|
||||
|
||||
@GET("api/front/search/users")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> searchUsersGlobal(
|
||||
@Query("keyword") String keyword,
|
||||
@Query("pageNum") int pageNum,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/search/live-rooms")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> searchLiveRooms(
|
||||
@Query("keyword") String keyword,
|
||||
@Query("categoryId") Integer categoryId,
|
||||
@Query("isLive") Integer isLive,
|
||||
@Query("pageNum") int pageNum,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/search/works")
|
||||
Call<ApiResponse<PageResponse<Map<String, Object>>>> searchWorksGlobal(
|
||||
@Query("keyword") String keyword,
|
||||
@Query("categoryId") Integer categoryId,
|
||||
@Query("pageNum") int pageNum,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@GET("api/front/search/all")
|
||||
Call<ApiResponse<Map<String, Object>>> searchAll(@Query("keyword") String keyword);
|
||||
|
||||
@GET("api/front/search/history")
|
||||
Call<ApiResponse<List<SearchHistoryResponse>>> getSearchHistory(
|
||||
@Query("searchType") Integer searchType,
|
||||
@Query("limit") int limit);
|
||||
|
||||
@DELETE("api/front/search/history")
|
||||
Call<ApiResponse<String>> clearSearchHistory(@Query("searchType") Integer searchType);
|
||||
|
||||
@DELETE("api/front/search/history/{historyId}")
|
||||
Call<ApiResponse<String>> deleteSearchHistory(@Path("historyId") long historyId);
|
||||
|
||||
@GET("api/front/search/hot")
|
||||
Call<ApiResponse<List<HotSearchResponse>>> getHotSearch(
|
||||
@Query("searchType") int searchType,
|
||||
@Query("limit") int limit);
|
||||
|
||||
@GET("api/front/search/suggestions")
|
||||
Call<ApiResponse<List<String>>> getSearchSuggestions(
|
||||
@Query("keyword") String keyword,
|
||||
@Query("searchType") Integer searchType,
|
||||
@Query("limit") int limit);
|
||||
|
||||
// ==================== 支付集成 ====================
|
||||
|
||||
@GET("api/front/gift/recharge/options")
|
||||
Call<ApiResponse<List<RechargeOptionResponse>>> getRechargeOptions();
|
||||
|
||||
@POST("api/front/gift/recharge/create")
|
||||
Call<ApiResponse<CreateRechargeResponse>> createRecharge(@Body CreateRechargeRequest body);
|
||||
|
||||
@GET("api/front/pay/alipay/queryPayResult")
|
||||
Call<ApiResponse<Boolean>> queryAliPayResult(@Query("orderNo") String orderNo);
|
||||
|
||||
@POST("api/front/pay/payment")
|
||||
Call<ApiResponse<OrderPayResultResponse>> payment(@Body OrderPayRequest body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* 热门搜索响应
|
||||
*/
|
||||
public class HotSearchResponse {
|
||||
@SerializedName("keyword")
|
||||
private String keyword;
|
||||
|
||||
@SerializedName("searchCount")
|
||||
private Integer searchCount;
|
||||
|
||||
@SerializedName("searchType")
|
||||
private Integer searchType; // 0-全部 1-用户 2-直播间 3-作品
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public Integer getSearchCount() {
|
||||
return searchCount;
|
||||
}
|
||||
|
||||
public void setSearchCount(Integer searchCount) {
|
||||
this.searchCount = searchCount;
|
||||
}
|
||||
|
||||
public Integer getSearchType() {
|
||||
return searchType;
|
||||
}
|
||||
|
||||
public void setSearchType(Integer searchType) {
|
||||
this.searchType = searchType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
/**
|
||||
* 消息表情回应数据模型
|
||||
*/
|
||||
public class MessageReaction {
|
||||
private String emoji; // 表情符号
|
||||
private int count; // 该表情的数量
|
||||
private boolean reactedByMe; // 当前用户是否已回应该表情
|
||||
|
||||
public MessageReaction() {
|
||||
}
|
||||
|
||||
public MessageReaction(String emoji, int count, boolean reactedByMe) {
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
this.reactedByMe = reactedByMe;
|
||||
}
|
||||
|
||||
public String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public void setEmoji(String emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public boolean isReactedByMe() {
|
||||
return reactedByMe;
|
||||
}
|
||||
|
||||
public void setReactedByMe(boolean reactedByMe) {
|
||||
this.reactedByMe = reactedByMe;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* 订单支付请求
|
||||
*/
|
||||
public class OrderPayRequest {
|
||||
|
||||
@SerializedName("orderNo")
|
||||
private String orderNo;
|
||||
|
||||
@SerializedName("payType")
|
||||
private String payType; // weixin-微信支付, alipay-支付宝支付, yue-余额支付
|
||||
|
||||
@SerializedName("payChannel")
|
||||
private String payChannel; // weixinAppAndroid-微信app安卓支付, appAliPay-App支付宝支付
|
||||
|
||||
@SerializedName("from")
|
||||
private String from; // android
|
||||
|
||||
public OrderPayRequest(String orderNo, String payType, String payChannel) {
|
||||
this.orderNo = orderNo;
|
||||
this.payType = payType;
|
||||
this.payChannel = payChannel;
|
||||
this.from = "android";
|
||||
}
|
||||
|
||||
public String getOrderNo() { return orderNo; }
|
||||
public String getPayType() { return payType; }
|
||||
public String getPayChannel() { return payChannel; }
|
||||
public String getFrom() { return from; }
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* 订单支付结果响应
|
||||
*/
|
||||
public class OrderPayResultResponse {
|
||||
|
||||
@SerializedName("status")
|
||||
private Boolean status;
|
||||
|
||||
@SerializedName("payType")
|
||||
private String payType;
|
||||
|
||||
@SerializedName("orderNo")
|
||||
private String orderNo;
|
||||
|
||||
@SerializedName("jsConfig")
|
||||
private Object jsConfig; // 微信/支付宝调起支付参数对象
|
||||
|
||||
public Boolean getStatus() { return status; }
|
||||
public String getPayType() { return payType; }
|
||||
public String getOrderNo() { return orderNo; }
|
||||
public Object getJsConfig() { return jsConfig; }
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* 搜索历史响应
|
||||
*/
|
||||
public class SearchHistoryResponse {
|
||||
@SerializedName("id")
|
||||
private Long id;
|
||||
|
||||
@SerializedName("keyword")
|
||||
private String keyword;
|
||||
|
||||
@SerializedName("searchType")
|
||||
private Integer searchType; // 1-用户 2-直播间 3-作品 4-消息
|
||||
|
||||
@SerializedName("createTime")
|
||||
private String createTime;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public Integer getSearchType() {
|
||||
return searchType;
|
||||
}
|
||||
|
||||
public void setSearchType(Integer searchType) {
|
||||
this.searchType = searchType;
|
||||
}
|
||||
|
||||
public String getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(String createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 作品请求模型
|
||||
*/
|
||||
public class WorksRequest {
|
||||
private Long id; // 作品ID(编辑时需要)
|
||||
private String title; // 作品标题
|
||||
private String description; // 作品描述
|
||||
private String type; // 作品类型:IMAGE 或 VIDEO
|
||||
private String coverUrl; // 封面图片URL
|
||||
private String videoUrl; // 视频URL(视频作品)
|
||||
private List<String> imageUrls; // 图片URL列表(图片作品)
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getCoverUrl() {
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
public void setCoverUrl(String coverUrl) {
|
||||
this.coverUrl = coverUrl;
|
||||
}
|
||||
|
||||
public String getVideoUrl() {
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
public void setVideoUrl(String videoUrl) {
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
public List<String> getImageUrls() {
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
public void setImageUrls(List<String> imageUrls) {
|
||||
this.imageUrls = imageUrls;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 作品响应模型
|
||||
*/
|
||||
public class WorksResponse {
|
||||
private Long id; // 作品ID
|
||||
private String title; // 作品标题
|
||||
private String description; // 作品描述
|
||||
private String type; // 作品类型:IMAGE 或 VIDEO
|
||||
private String coverUrl; // 封面图片URL
|
||||
private String videoUrl; // 视频URL(视频作品)
|
||||
private List<String> imageUrls; // 图片URL列表(图片作品)
|
||||
private Integer likeCount; // 点赞数
|
||||
private Integer collectCount; // 收藏数
|
||||
private Integer commentCount; // 评论数
|
||||
private Integer viewCount; // 观看数
|
||||
private Integer shareCount; // 分享数
|
||||
private Long publishTime; // 发布时间戳
|
||||
private Integer userId; // 作者用户ID
|
||||
private String userName; // 作者用户名
|
||||
private String userAvatar; // 作者头像URL
|
||||
private Boolean isLiked; // 当前用户是否已点赞
|
||||
private Boolean isCollected; // 当前用户是否已收藏
|
||||
private Boolean isOwner; // 是否是当前用户的作品
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getCoverUrl() {
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
public void setCoverUrl(String coverUrl) {
|
||||
this.coverUrl = coverUrl;
|
||||
}
|
||||
|
||||
public String getVideoUrl() {
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
public void setVideoUrl(String videoUrl) {
|
||||
this.videoUrl = videoUrl;
|
||||
}
|
||||
|
||||
public List<String> getImageUrls() {
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
public void setImageUrls(List<String> imageUrls) {
|
||||
this.imageUrls = imageUrls;
|
||||
}
|
||||
|
||||
public Integer getLikeCount() {
|
||||
return likeCount != null ? likeCount : 0;
|
||||
}
|
||||
|
||||
public void setLikeCount(Integer likeCount) {
|
||||
this.likeCount = likeCount;
|
||||
}
|
||||
|
||||
public Integer getCollectCount() {
|
||||
return collectCount != null ? collectCount : 0;
|
||||
}
|
||||
|
||||
public void setCollectCount(Integer collectCount) {
|
||||
this.collectCount = collectCount;
|
||||
}
|
||||
|
||||
public Integer getCommentCount() {
|
||||
return commentCount != null ? commentCount : 0;
|
||||
}
|
||||
|
||||
public void setCommentCount(Integer commentCount) {
|
||||
this.commentCount = commentCount;
|
||||
}
|
||||
|
||||
public Integer getViewCount() {
|
||||
return viewCount != null ? viewCount : 0;
|
||||
}
|
||||
|
||||
public void setViewCount(Integer viewCount) {
|
||||
this.viewCount = viewCount;
|
||||
}
|
||||
|
||||
public Integer getShareCount() {
|
||||
return shareCount != null ? shareCount : 0;
|
||||
}
|
||||
|
||||
public void setShareCount(Integer shareCount) {
|
||||
this.shareCount = shareCount;
|
||||
}
|
||||
|
||||
public Long getPublishTime() {
|
||||
return publishTime;
|
||||
}
|
||||
|
||||
public void setPublishTime(Long publishTime) {
|
||||
this.publishTime = publishTime;
|
||||
}
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getUserAvatar() {
|
||||
return userAvatar;
|
||||
}
|
||||
|
||||
public void setUserAvatar(String userAvatar) {
|
||||
this.userAvatar = userAvatar;
|
||||
}
|
||||
|
||||
public Boolean getIsLiked() {
|
||||
return isLiked != null ? isLiked : false;
|
||||
}
|
||||
|
||||
public void setIsLiked(Boolean isLiked) {
|
||||
this.isLiked = isLiked;
|
||||
}
|
||||
|
||||
public Boolean getIsCollected() {
|
||||
return isCollected != null ? isCollected : false;
|
||||
}
|
||||
|
||||
public void setIsCollected(Boolean isCollected) {
|
||||
this.isCollected = isCollected;
|
||||
}
|
||||
|
||||
public Boolean getIsOwner() {
|
||||
return isOwner != null ? isOwner : false;
|
||||
}
|
||||
|
||||
public void setIsOwner(Boolean isOwner) {
|
||||
this.isOwner = isOwner;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
/**
|
||||
* 作品搜索请求模型
|
||||
*/
|
||||
public class WorksSearchRequest {
|
||||
private String keyword; // 搜索关键词
|
||||
private String type; // 作品类型:IMAGE 或 VIDEO
|
||||
private Integer page; // 页码
|
||||
private Integer pageSize; // 每页数量
|
||||
private String sortBy; // 排序字段:publishTime, likeCount, viewCount
|
||||
private String sortOrder; // 排序方式:asc, desc
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Integer getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(Integer page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public Integer getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(Integer pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public String getSortBy() {
|
||||
return sortBy;
|
||||
}
|
||||
|
||||
public void setSortBy(String sortBy) {
|
||||
this.sortBy = sortBy;
|
||||
}
|
||||
|
||||
public String getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(String sortOrder) {
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/white"/>
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp"/>
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#F0F0F0"/>
|
||||
<corners android:radius="12dp"/>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#E0E0E0"/>
|
||||
</shape>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#E3F2FD"/>
|
||||
<corners android:radius="12dp"/>
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="#2196F3"/>
|
||||
</shape>
|
||||
|
|
@ -136,6 +136,16 @@
|
|||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/blockedRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/bottom_sheet_background">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择表情回应"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:gravity="center"
|
||||
android:paddingBottom="16dp"/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/emojiGrid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="6"
|
||||
android:rowCount="3"
|
||||
android:alignmentMode="alignBounds"
|
||||
android:useDefaultMargins="true">
|
||||
|
||||
<!-- 常用表情 -->
|
||||
<TextView
|
||||
android:id="@+id/emoji_like"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="👍"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_love"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="❤️"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_laugh"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="😂"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_wow"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="😮"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_sad"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="😢"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_angry"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="😠"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_fire"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="🔥"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_clap"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="👏"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_thinking"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="🤔"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_party"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="🎉"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_star"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="⭐"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_check"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:text="✅"
|
||||
android:textSize="32sp"
|
||||
android:gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"/>
|
||||
|
||||
</GridLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -46,6 +46,14 @@
|
|||
android:background="@drawable/message_bubble_incoming"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<!-- 表情回应区域 -->
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/reactionsContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- 时间 -->
|
||||
<TextView
|
||||
android:id="@+id/timeText"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@
|
|||
android:background="@drawable/message_bubble_outgoing"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
<!-- 表情回应区域 -->
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/reactionsContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- 时间和状态 -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/reaction_background"
|
||||
android:padding="4dp"
|
||||
android:layout_margin="2dp"
|
||||
android:gravity="center"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emojiText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="👍"
|
||||
android:textSize="16sp"
|
||||
android:paddingEnd="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/countText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
# Android 直播应用新 UI 设计说明
|
||||
|
||||
## 🎨 设计概览
|
||||
|
||||
已将 Android 应用首页重新设计为现代化的瀑布流布局,参考了主流直播应用的设计风格。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成的修改
|
||||
|
||||
### 1. 新增布局文件
|
||||
|
||||
#### 主界面布局 (`activity_main.xml`)
|
||||
- **顶部导航栏**:
|
||||
- 左侧:菜单图标
|
||||
- 中间:关注/发现/附近 标签页
|
||||
- 右侧:消息和通知图标
|
||||
|
||||
- **搜索栏**:
|
||||
- 搜索框 + 语音搜索按钮
|
||||
- 圆角设计,灰色背景
|
||||
|
||||
- **分类标签**:
|
||||
- 横向滚动
|
||||
- 推荐、颜值、才艺、户外、娱乐、聊天
|
||||
- 选中状态:紫色背景 + 白色文字
|
||||
- 未选中:灰色背景 + 灰色文字
|
||||
|
||||
- **内容区域**:
|
||||
- 瀑布流布局(2列)
|
||||
- RecyclerView + StaggeredGridLayoutManager
|
||||
|
||||
- **底部导航栏**:
|
||||
- 首页、发现、开播(中间大按钮)、消息、我的
|
||||
- 固定在底部,白色背景,带阴影
|
||||
|
||||
#### 直播间卡片布局 (`item_room_waterfall.xml`)
|
||||
- **封面图**:3:4 比例
|
||||
- **直播标签**:左上角红色标签(直播中时显示)
|
||||
- **观看人数**:右上角半透明黑色背景
|
||||
- **底部信息**:
|
||||
- 标题(最多2行)
|
||||
- 主播头像 + 名称
|
||||
- 热门标签(观看人数>100时显示)
|
||||
|
||||
### 2. 新增 Drawable 资源
|
||||
|
||||
| 文件名 | 用途 |
|
||||
|--------|------|
|
||||
| `search_background.xml` | 搜索框背景(圆角灰色) |
|
||||
| `category_selected.xml` | 选中的分类标签背景(紫色) |
|
||||
| `category_normal.xml` | 未选中的分类标签背景(灰色) |
|
||||
| `live_badge_background.xml` | 直播标签背景(红色) |
|
||||
| `viewer_count_background.xml` | 观看人数背景(半透明黑色) |
|
||||
| `hot_badge_background.xml` | 热门标签背景(红色边框) |
|
||||
|
||||
### 3. 新增 Java 类
|
||||
|
||||
#### `WaterfallRoomsAdapter.java`
|
||||
- 瀑布流适配器
|
||||
- 支持直播状态显示
|
||||
- 支持观看人数显示
|
||||
- 支持热门标签(观看人数>100)
|
||||
|
||||
### 4. 修改的文件
|
||||
|
||||
#### `MainActivity.java`
|
||||
- 使用 `WaterfallRoomsAdapter` 替代 `RoomsAdapter`
|
||||
- 使用 `StaggeredGridLayoutManager` 实现瀑布流(2列)
|
||||
- 保持原有的轮询和点击逻辑
|
||||
|
||||
#### `themes.xml`
|
||||
- 添加 `CategoryChip` 样式
|
||||
- 修改状态栏和导航栏颜色为白色
|
||||
- 启用浅色状态栏图标
|
||||
|
||||
---
|
||||
|
||||
## 🎨 设计细节
|
||||
|
||||
### 颜色方案
|
||||
- **主色调**:紫色 (`#6200EE`)
|
||||
- **强调色**:红色 (`#E53935`) - 用于直播标签
|
||||
- **背景色**:浅灰 (`#F5F5F5`)
|
||||
- **文字色**:
|
||||
- 主要文字:`#333333`
|
||||
- 次要文字:`#666666`
|
||||
- 提示文字:`#999999`
|
||||
|
||||
### 圆角设计
|
||||
- 搜索框:24dp
|
||||
- 分类标签:16dp
|
||||
- 直播间卡片:12dp
|
||||
- 直播标签:4dp
|
||||
|
||||
### 间距规范
|
||||
- 页面边距:16dp
|
||||
- 卡片间距:8dp
|
||||
- 元素内边距:12dp
|
||||
|
||||
---
|
||||
|
||||
## 📱 功能说明
|
||||
|
||||
### 当前已实现
|
||||
- ✅ 瀑布流布局展示直播间
|
||||
- ✅ 直播状态标签
|
||||
- ✅ 观看人数显示
|
||||
- ✅ 点击进入直播间
|
||||
- ✅ 自动刷新列表(5秒轮询)
|
||||
- ✅ 开播按钮(底部导航中间)
|
||||
|
||||
### 待实现(UI已准备好)
|
||||
- ⏳ 搜索功能
|
||||
- ⏳ 语音搜索
|
||||
- ⏳ 分类筛选
|
||||
- ⏳ 关注/发现/附近 标签页切换
|
||||
- ⏳ 消息和通知功能
|
||||
- ⏳ 底部导航其他页面
|
||||
|
||||
---
|
||||
|
||||
## 🔧 如何编译和运行
|
||||
|
||||
### 1. 清理项目
|
||||
```bash
|
||||
cd android-app
|
||||
gradlew clean
|
||||
```
|
||||
|
||||
### 2. 编译
|
||||
```bash
|
||||
gradlew assembleDebug
|
||||
```
|
||||
|
||||
### 3. 安装到模拟器
|
||||
```bash
|
||||
gradlew installDebug
|
||||
```
|
||||
|
||||
或者在 Android Studio 中直接点击 ▶️ Run
|
||||
|
||||
---
|
||||
|
||||
## 📸 布局预览
|
||||
|
||||
### 主界面结构
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ ☰ 关注 发现 附近 ✉ 🔔 │ ← 顶部导航
|
||||
├─────────────────────────────┤
|
||||
│ 🔍 搜索主播/房间/标签 🎤 │ ← 搜索栏
|
||||
├─────────────────────────────┤
|
||||
│ [推荐][颜值][才艺][户外]... │ ← 分类标签
|
||||
├─────────────────────────────┤
|
||||
│ ┌──────┐ ┌──────┐ │
|
||||
│ │ 封面 │ │ 封面 │ │
|
||||
│ │ 图片 │ │ 图片 │ │
|
||||
│ │ │ │ │ │ ← 瀑布流内容
|
||||
│ │ 标题 │ │ 标题 │ │
|
||||
│ │ 主播 │ │ 主播 │ │
|
||||
│ └──────┘ └──────┘ │
|
||||
│ ┌──────┐ ┌──────┐ │
|
||||
│ │ ... │ │ ... │ │
|
||||
└─────────────────────────────┘
|
||||
│ 🏠 🔍 ➕ 💬 👤 │ ← 底部导航
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 直播间卡片结构
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ [直播中] 👁 123│ ← 标签和观看人数
|
||||
│ │
|
||||
│ 封面图片 │
|
||||
│ │
|
||||
│ │
|
||||
├─────────────────┤
|
||||
│ 直播间标题 │
|
||||
│ 👤 主播名称 [热] │ ← 主播信息
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 已知问题
|
||||
|
||||
1. **封面图片**:当前使用默认图标,需要后端提供封面图 URL
|
||||
2. **主播头像**:当前使用默认图标,需要后端提供头像 URL
|
||||
3. **分类筛选**:UI已完成,但功能未实现
|
||||
4. **搜索功能**:UI已完成,但功能未实现
|
||||
|
||||
---
|
||||
|
||||
## 🔄 如何恢复旧版 UI
|
||||
|
||||
如果需要恢复旧版 UI:
|
||||
|
||||
```bash
|
||||
cd android-app/app/src/main/res/layout
|
||||
Copy-Item activity_main_old.xml activity_main.xml -Force
|
||||
Copy-Item item_room_old.xml item_room.xml -Force
|
||||
```
|
||||
|
||||
然后在 `MainActivity.java` 中:
|
||||
- 将 `WaterfallRoomsAdapter` 改回 `RoomsAdapter`
|
||||
- 将 `StaggeredGridLayoutManager` 改回 `LinearLayoutManager`
|
||||
|
||||
---
|
||||
|
||||
## 📝 后续优化建议
|
||||
|
||||
### 1. 添加封面图支持
|
||||
- 后端返回封面图 URL
|
||||
- 使用 Glide 或 Picasso 加载图片
|
||||
|
||||
### 2. 实现分类筛选
|
||||
- 点击分类标签时筛选对应类型的直播间
|
||||
- 添加分类字段到 Room 模型
|
||||
|
||||
### 3. 实现搜索功能
|
||||
- 点击搜索框打开搜索页面
|
||||
- 支持搜索主播名称和直播间标题
|
||||
|
||||
### 4. 添加下拉刷新
|
||||
- 使用 SwipeRefreshLayout
|
||||
- 手动刷新直播间列表
|
||||
|
||||
### 5. 添加加载更多
|
||||
- 监听滚动到底部
|
||||
- 分页加载直播间数据
|
||||
|
||||
---
|
||||
|
||||
**设计完成!现在可以编译运行查看新的 UI 效果了!** 🎉
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
# 项目未完成功能清单
|
||||
---
|
||||
|
||||
## ✅ 刚刚完成的功能
|
||||
|
||||
### 通知功能(前端UI)✅
|
||||
- ✅ 通知列表页面 (NotificationsActivity)
|
||||
- ✅ 通知分类(系统、互动、关注、私信、直播)
|
||||
- ✅ 通知设置页面 (NotificationSettingsActivity)
|
||||
- ✅ 本地通知功能 (LocalNotificationManager)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 高优先级未完成功能(前端可独立完成)
|
||||
|
||||
### 1. **数据持久化(Room数据库)** ⭐⭐⭐
|
||||
**状态**: 未开始
|
||||
**位置**: 文档第472行
|
||||
|
||||
- [ ] 引入 Room 数据库依赖
|
||||
- [ ] 创建数据库实体(Room、User、Message、Notification等)
|
||||
- [ ] 创建 DAO 接口
|
||||
- [ ] 创建数据库类
|
||||
- [ ] 实现 Repository 模式
|
||||
- [ ] 缓存直播间列表到本地
|
||||
- [ ] 缓存用户信息
|
||||
- [ ] 实现搜索历史存储
|
||||
- [ ] 实现观看历史存储
|
||||
- [ ] 实现消息记录缓存
|
||||
|
||||
**预计工作量**: 3-5天
|
||||
|
||||
---
|
||||
|
||||
### 2. **顶部标签页功能** ⭐⭐
|
||||
**状态**: ✅ 已完成
|
||||
**位置**: MainActivity.java 第265-291行
|
||||
|
||||
- [x] 实现关注页面(显示已关注主播的直播)
|
||||
- [x] 实现发现页面(推荐算法前端实现)
|
||||
- [x] 实现附近页面(使用模拟位置数据)
|
||||
- [x] 添加位置权限申请(即使后端未就绪)
|
||||
|
||||
**完成说明**:
|
||||
- 修改MainActivity,让顶部标签页在当前页面切换内容,而不是跳转到新页面
|
||||
- 关注页面:显示已关注主播的直播列表(基于FollowingListActivity的数据结构)
|
||||
- 发现页面:实现前端推荐算法(优先显示正在直播的房间,按类型排序)
|
||||
- 附近页面:使用模拟位置数据,添加位置权限检查和申请逻辑
|
||||
- 在AndroidManifest.xml中添加了位置权限声明
|
||||
- 实现了位置权限申请对话框和权限被拒绝时的友好提示
|
||||
|
||||
**预计工作量**: 3-4天(已完成)
|
||||
|
||||
---
|
||||
|
||||
### 3. **搜索功能增强** ⭐
|
||||
**状态**: 基础功能完成,增强功能未实现
|
||||
**位置**: SearchActivity.java
|
||||
|
||||
- [ ] 实现搜索历史(本地存储,使用SharedPreferences或Room)
|
||||
- [ ] 实现热门搜索(模拟数据)
|
||||
- [ ] 优化搜索性能(防抖、缓存)
|
||||
- [ ] 添加搜索建议(自动补全)
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
---
|
||||
|
||||
### 4. **作品功能(前端UI)** ⭐
|
||||
**状态**: UI已存在,功能未实现
|
||||
**位置**: ProfileActivity.java 第254行
|
||||
|
||||
- [ ] 实现作品列表UI(已有tabWorks布局)
|
||||
- [ ] 实现作品发布UI(数据仅本地存储)
|
||||
- [ ] 实现作品详情页面
|
||||
- [ ] 实现作品编辑UI
|
||||
- [ ] 作品数据模型和适配器
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
---
|
||||
|
||||
### 5. **许愿树核心功能** ⭐
|
||||
**状态**: 仅UI完成,核心功能未实现
|
||||
**位置**: WishTreeActivity.java
|
||||
|
||||
- [ ] 实现许愿功能
|
||||
- [ ] 实现抽奖功能
|
||||
- [ ] 实现许愿列表展示
|
||||
- [ ] 实现倒计时结束后的事件处理
|
||||
- [ ] 许愿数据模型和存储
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
---
|
||||
|
||||
## 🟡 中优先级未完成功能
|
||||
|
||||
### 6. **占位页面功能实现**
|
||||
**状态**: 多个功能跳转到TabPlaceholderActivity
|
||||
**位置**: TabPlaceholderActivity.java
|
||||
|
||||
以下功能需要实现(目前都是占位页面):
|
||||
|
||||
- [ ] 语音匹配 (VoiceMatchActivity) - 已有Activity但功能未实现
|
||||
- [ ] 心动信号 (HeartbeatSignalActivity) - 已有Activity但功能未实现
|
||||
- [ ] 在线处对象 (OnlineDatingActivity) - 已有Activity但功能未实现
|
||||
- [ ] 找人玩游戏 (FindGameActivity) - 已有Activity但功能未实现
|
||||
- [ ] 一起KTV (KTVTogetherActivity) - 已有Activity但功能未实现
|
||||
- [ ] 你画我猜 (DrawGuessActivity) - 已有Activity但功能未实现
|
||||
- [ ] 和平精英 (PeaceEliteActivity) - 已有Activity但功能未实现
|
||||
- [ ] 桌子游 (TableGamesActivity) - 已有Activity但功能未实现
|
||||
- [ ] 公园勋章 - 跳转到TabPlaceholderActivity
|
||||
- [ ] 加好友 - 跳转到TabPlaceholderActivity
|
||||
- [ ] 定位/发现 - 跳转到TabPlaceholderActivity
|
||||
- [ ] 附近直播 - TabPlaceholderActivity中显示"待接入"
|
||||
- [ ] 热门地点 - TabPlaceholderActivity中显示"待接入"
|
||||
- [ ] 榜单功能 - TabPlaceholderActivity中显示"待接入"
|
||||
- [ ] 话题功能 - TabPlaceholderActivity中显示"待接入"
|
||||
|
||||
**预计工作量**: 每个功能1-2天,总计15-30天
|
||||
|
||||
---
|
||||
|
||||
### 7. **设置页面完善** ⭐ ✅ 已完成
|
||||
**状态**: 功能已完善
|
||||
**位置**: SettingsPageActivity.java
|
||||
|
||||
- [x] 完善设置页面功能
|
||||
- [x] 添加应用设置(缓存清理功能实现)
|
||||
- [x] 添加账号设置UI(修改密码、绑定手机号等)
|
||||
- [x] 添加隐私设置UI(黑名单、权限管理等)
|
||||
- [x] 实现帮助中心内容
|
||||
- [x] 实现关于页面内容
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `CacheManager` 缓存管理工具类
|
||||
- 实现了所有设置页面的功能对话框
|
||||
- 创建了对话框布局文件
|
||||
- 所有功能都已实现(部分功能待接入后端API)
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
---
|
||||
|
||||
### 8. **引导页面和帮助** ⭐
|
||||
**状态**: 未实现
|
||||
**位置**: 无
|
||||
|
||||
- [ ] 实现首次启动引导页(ViewPager2)
|
||||
- [ ] 添加功能介绍页面
|
||||
- [ ] 添加权限说明页面
|
||||
- [ ] 实现帮助中心
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
---
|
||||
|
||||
## 🟢 低优先级未完成功能(体验增强)
|
||||
|
||||
### 9. **过渡动画和交互优化** ⭐
|
||||
**状态**: 未实现
|
||||
|
||||
- [ ] 添加 Activity 过渡动画
|
||||
- [ ] 实现共享元素过渡
|
||||
- [ ] 优化页面内动画
|
||||
- [ ] 添加触觉反馈(Haptic Feedback)
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
---
|
||||
|
||||
### 10. **深色模式支持** ⭐
|
||||
**状态**: 未实现
|
||||
|
||||
- [ ] 创建深色模式资源
|
||||
- [ ] 适配所有页面颜色
|
||||
- [ ] 添加手动切换功能
|
||||
- [ ] 测试深色模式显示
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
---
|
||||
|
||||
### 11. **多屏幕适配** ⭐
|
||||
**状态**: 未实现
|
||||
|
||||
- [ ] 优化平板布局
|
||||
- [ ] 添加横屏布局
|
||||
- [ ] 测试不同屏幕尺寸
|
||||
- [ ] 优化不同分辨率显示
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
---
|
||||
|
||||
## ⚪ 架构和优化(持续进行)
|
||||
|
||||
### 12. **前端架构优化** ⭐⭐
|
||||
**状态**: 未开始
|
||||
|
||||
- [ ] 引入 MVVM 架构(ViewModel + LiveData)
|
||||
- [ ] 实现 Repository 模式(本地数据源)
|
||||
- [ ] 提取公共基类 Activity
|
||||
- [ ] 创建工具类库
|
||||
- [ ] 引入依赖注入(Hilt,可选)
|
||||
|
||||
**预计工作量**: 5-7天
|
||||
|
||||
---
|
||||
|
||||
### 13. **性能优化** ⭐
|
||||
**状态**: 部分完成
|
||||
|
||||
- [ ] 优化图片加载(Glide配置优化)
|
||||
- [ ] 优化列表滚动性能
|
||||
- [ ] 实现请求去重
|
||||
- [ ] 优化内存使用
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
---
|
||||
|
||||
### 14. **代码质量提升** ⭐
|
||||
**状态**: 持续进行
|
||||
|
||||
- [ ] 提取硬编码字符串到资源文件
|
||||
- [ ] 添加代码注释
|
||||
- [ ] 统一代码风格
|
||||
- [ ] 重构重复代码
|
||||
|
||||
**预计工作量**: 持续进行
|
||||
|
||||
---
|
||||
|
||||
### 15. **测试** ⭐
|
||||
**状态**: 未开始
|
||||
|
||||
- [ ] 编写单元测试(工具类)
|
||||
- [ ] 编写 UI 测试(关键流程)
|
||||
- [ ] 性能测试
|
||||
- [ ] 兼容性测试
|
||||
|
||||
**预计工作量**: 持续进行
|
||||
|
||||
---
|
||||
|
||||
## ❌ 需要后端支持的功能(暂不实现)
|
||||
|
||||
以下功能需要后端支持,建议后端开发完成后再实现:
|
||||
|
||||
- ❌ 后端API完整集成(等待后端接口)
|
||||
- ❌ 实时通信(WebSocket,等待后端)
|
||||
- ❌ 真实数据同步(等待后端)
|
||||
- ❌ 用户登录/注册(等待后端)
|
||||
- ❌ 支付功能(等待后端和支付SDK)
|
||||
- ❌ 推流功能(如需要,等待推流SDK集成)
|
||||
- ❌ 礼物打赏功能(等待后端和支付SDK)
|
||||
- ❌ 弹幕功能(等待WebSocket服务)
|
||||
|
||||
---
|
||||
|
||||
## 📊 完成度统计
|
||||
|
||||
### 前端可独立完成的功能
|
||||
- **已完成**: 约 40%
|
||||
- **进行中**: 约 10%
|
||||
- **未开始**: 约 50%
|
||||
|
||||
### 核心功能模块
|
||||
- ✅ **直播相关**: 85% 完成
|
||||
- ✅ **社交功能**: 75% 完成
|
||||
- ⚠️ **个人中心**: 70% 完成(作品功能缺失)
|
||||
- ⚠️ **发现功能**: 60% 完成(搜索增强、标签页缺失)
|
||||
- ⚠️ **特色功能**: 50% 完成(许愿树、占位功能缺失)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 建议优先完成顺序
|
||||
|
||||
### 第一周
|
||||
1. **数据持久化(Room数据库)** - 3-5天
|
||||
- 这是基础架构,其他功能会依赖它
|
||||
|
||||
### 第二周
|
||||
2. **搜索功能增强** - 2-3天
|
||||
3. **作品功能** - 3-4天
|
||||
|
||||
### 第三周
|
||||
4. **顶部标签页功能** - 3-4天
|
||||
5. **许愿树核心功能** - 3-4天
|
||||
|
||||
### 第四周
|
||||
6. **设置页面完善** - 2-3天
|
||||
7. **引导页面** - 2-3天
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **数据持久化**是最重要的基础功能,建议优先完成
|
||||
2. **占位页面功能**数量较多,可以根据实际需求选择性实现
|
||||
3. **架构优化**可以在功能完善过程中逐步进行
|
||||
4. **需要后端支持的功能**可以先做UI,使用模拟数据
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024年
|
||||
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# Android 直播应用问题修复说明
|
||||
|
||||
## 问题诊断
|
||||
|
||||
### 1. RTMP 地址格式错误 ❌
|
||||
**问题**:后端返回的 RTMP 地址不完整
|
||||
- 错误格式:`rtmp://10.0.2.2:1935/live`
|
||||
- 正确格式:`rtmp://10.0.2.2:1935/live/{streamKey}`
|
||||
|
||||
**原因**:`live-streaming/server/utils/streamUrl.js` 中缺少 streamKey
|
||||
|
||||
### 2. Android 应用功能误解 ⚠️
|
||||
**重要**:你的 Android 应用**不支持推流**!
|
||||
|
||||
当前应用功能:
|
||||
- ✅ 创建直播间
|
||||
- ✅ 查看直播列表
|
||||
- ✅ 观看直播(播放 HLS/FLV 流)
|
||||
- ❌ **不能**从 Android 设备推流
|
||||
|
||||
正确的使用流程:
|
||||
1. 在 Android 应用中创建直播间
|
||||
2. 获取 RTMP 推流地址
|
||||
3. **在电脑上用 OBS Studio 推流**
|
||||
4. 在 Android 应用中观看直播
|
||||
|
||||
## 已完成的修复 ✅
|
||||
|
||||
### 1. 修复后端 RTMP 地址
|
||||
修改了 `live-streaming/server/utils/streamUrl.js`:
|
||||
```javascript
|
||||
// 修复前
|
||||
rtmp: `rtmp://${host}:${rtmpPort}/live`,
|
||||
|
||||
// 修复后
|
||||
rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`,
|
||||
```
|
||||
|
||||
### 2. 更新 Android 应用提示
|
||||
修改了 `MainActivity.java` 中的提示信息,明确说明需要用 OBS 推流。
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 步骤 1:启动后端服务器
|
||||
```bash
|
||||
cd live-streaming
|
||||
npm start
|
||||
```
|
||||
|
||||
确认服务器运行:
|
||||
```bash
|
||||
curl http://localhost:3001/health
|
||||
```
|
||||
|
||||
### 步骤 2:重新构建 Android 应用
|
||||
```bash
|
||||
cd android-app
|
||||
gradlew clean assembleDebug
|
||||
gradlew installDebug
|
||||
```
|
||||
|
||||
或在 Android Studio 中点击 "Run"
|
||||
|
||||
### 步骤 3:创建直播间
|
||||
1. 在 Android 应用中点击"开始直播"
|
||||
2. 填写直播间标题和主播名称
|
||||
3. 创建成功后会显示 RTMP 推流地址
|
||||
|
||||
### 步骤 4:使用 OBS 推流
|
||||
1. 打开 OBS Studio
|
||||
2. 设置 → 推流
|
||||
3. 服务器:`rtmp://localhost:1935/live/{你的streamKey}`
|
||||
- 或直接填写应用显示的完整地址
|
||||
4. 开始推流
|
||||
|
||||
### 步骤 5:在 Android 应用中观看
|
||||
推流成功后,直播间会显示"直播中"状态,点击进入即可观看。
|
||||
|
||||
## Android 模拟器网络说明
|
||||
|
||||
在 Android 模拟器中:
|
||||
- `10.0.2.2` = 宿主机的 `localhost`
|
||||
- `10.0.2.16` = 模拟器自己的 IP
|
||||
|
||||
所以:
|
||||
- Android 应用访问 API:`http://10.0.2.2:3001/api/`
|
||||
- OBS 推流地址:`rtmp://localhost:1935/live/{streamKey}`
|
||||
- Android 观看地址:`http://10.0.2.2:8080/live/{streamKey}.flv`
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么 Android 应用不能直接推流?
|
||||
A: 从 Android 设备推流需要:
|
||||
- 摄像头/麦克风权限
|
||||
- RTMP 推流库(如 librtmp)
|
||||
- 视频编码处理
|
||||
- 当前应用没有实现这些功能
|
||||
|
||||
### Q: 如果想让 Android 应用支持推流怎么办?
|
||||
A: 需要添加推流功能,可以使用:
|
||||
- [yasea](https://github.com/begeekmyfriend/yasea) - Android RTMP 推流库
|
||||
- [LiveVideoBroadcaster](https://github.com/ant-media/LiveVideoBroadcaster) - Ant Media 的推流方案
|
||||
|
||||
### Q: 推流后 Android 应用看不到直播?
|
||||
A: 检查:
|
||||
1. OBS 是否成功推流(查看 OBS 状态栏)
|
||||
2. 后端服务器是否收到推流(查看服务器日志)
|
||||
3. RTMP 地址和 streamKey 是否正确
|
||||
4. 防火墙是否阻止了端口 1935 和 8080
|
||||
|
||||
### Q: 出现"服务器异常"错误?
|
||||
A: 可能原因:
|
||||
1. 后端服务器未启动
|
||||
2. 网络连接问题
|
||||
3. API 地址配置错误
|
||||
4. 查看 Android Studio 的 Logcat 获取详细错误信息
|
||||
|
||||
## 防火墙配置
|
||||
|
||||
如果遇到连接问题,添加防火墙规则:
|
||||
```powershell
|
||||
netsh advfirewall firewall add rule name="Node.js API" dir=in action=allow protocol=TCP localport=3001
|
||||
netsh advfirewall firewall add rule name="RTMP Server" dir=in action=allow protocol=TCP localport=1935
|
||||
netsh advfirewall firewall add rule name="HTTP-FLV" dir=in action=allow protocol=TCP localport=8080
|
||||
```
|
||||
|
||||
## 相关文件
|
||||
- 后端服务器:`live-streaming/server/index.js`
|
||||
- RTMP 地址生成:`live-streaming/server/utils/streamUrl.js`
|
||||
- Android 配置:`android-app/app/build.gradle.kts`
|
||||
- Android 主界面:`android-app/app/src/main/java/com/example/livestreaming/MainActivity.java`
|
||||
- Android 直播间:`android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java`
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
# 防抖功能使用指南
|
||||
|
||||
## 已创建的防抖工具类
|
||||
|
||||
已创建 `DebounceClickListener.java`,位于:
|
||||
```
|
||||
android-app/app/src/main/java/com/example/livestreaming/DebounceClickListener.java
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 基本用法(默认500ms防抖)
|
||||
|
||||
**之前的代码:**
|
||||
```java
|
||||
button.setOnClickListener(v -> {
|
||||
// 处理点击事件
|
||||
doSomething();
|
||||
});
|
||||
```
|
||||
|
||||
**修改后的代码:**
|
||||
```java
|
||||
button.setOnClickListener(new DebounceClickListener() {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
// 处理点击事件
|
||||
doSomething();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 自定义防抖时间
|
||||
|
||||
```java
|
||||
button.setOnClickListener(new DebounceClickListener(1000) { // 1秒防抖
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
doSomething();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 已添加防抖的位置
|
||||
|
||||
### WorkDetailActivity.java ✅
|
||||
- [x] 返回按钮 (backButton)
|
||||
- [x] 操作菜单按钮 (actionButton)
|
||||
- [x] 点赞按钮 (likeButtonContainer)
|
||||
- [x] 收藏按钮 (favoriteButtonContainer)
|
||||
- [x] 评论按钮 (commentButtonContainer)
|
||||
- [x] 描述文本点击 (descriptionText)
|
||||
|
||||
## 需要添加防抖的位置
|
||||
|
||||
### 高优先级(用户交互频繁的按钮)
|
||||
|
||||
#### MainActivity.java
|
||||
- [ ] 菜单按钮 (menuButton) - 第312行
|
||||
- [ ] 头像按钮 (avatarButton) - 第322行
|
||||
- [ ] 通知图标 (notificationIcon) - 第335行
|
||||
- [ ] 添加直播按钮 (fabAddLive) - 第429行
|
||||
- [ ] 麦克风/搜索图标 (micIcon) - 第505行
|
||||
- [ ] 创建直播间对话框确认按钮 - 第898行
|
||||
- [ ] 复制推流地址按钮 (copyAddressBtn) - 第1053行
|
||||
- [ ] 复制推流密钥按钮 (copyKeyBtn) - 第1058行
|
||||
|
||||
#### ConversationActivity.java
|
||||
- [ ] 返回按钮 (backButton) - 第64行
|
||||
- [ ] 发送按钮 (sendButton) - 第175行
|
||||
|
||||
#### EditProfileActivity.java
|
||||
- [ ] 返回按钮 (backButton) - 第106行
|
||||
- [ ] 取消按钮 (cancelButton) - 第107行
|
||||
- [ ] 头像行点击 (avatarRow) - 第114行
|
||||
- [ ] 头像预览点击 (avatarPreview) - 第117行
|
||||
- [ ] 保存按钮 (saveButton) - 第145行
|
||||
- [ ] 选择图片按钮 (pick) - 第308行
|
||||
- [ ] 拍照按钮 (camera) - 第313行
|
||||
- [ ] 取消按钮 (cancel) - 第321行
|
||||
- [ ] 性别输入点击 (inputGender) - 第351行
|
||||
- [ ] 位置输入点击 (inputLocation) - 第366行
|
||||
- [ ] 位置确认按钮 (confirmButton) - 第438行
|
||||
- [ ] 位置取消按钮 (cancelButton) - 第444行
|
||||
- [ ] 生日输入点击 (inputBirthday) - 第466行
|
||||
- [ ] 生日确认按钮 (confirmButton) - 第547行
|
||||
- [ ] 生日取消按钮 (cancelButton) - 第572行
|
||||
|
||||
#### LoginActivity.java
|
||||
- [ ] 登录按钮
|
||||
- [ ] 注册跳转按钮
|
||||
|
||||
#### RegisterActivity.java
|
||||
- [ ] 注册按钮
|
||||
- [ ] 返回登录按钮
|
||||
|
||||
#### ProfileActivity.java
|
||||
- [ ] 返回按钮
|
||||
- [ ] 编辑资料按钮
|
||||
- [ ] 关注/取消关注按钮
|
||||
- [ ] 作品点击
|
||||
|
||||
#### PublishWorkActivity.java
|
||||
- [ ] 返回按钮
|
||||
- [ ] 选择图片/视频按钮
|
||||
- [ ] 发布按钮
|
||||
|
||||
#### SearchActivity.java
|
||||
- [ ] 返回按钮
|
||||
- [ ] 搜索按钮
|
||||
- [ ] 搜索建议项点击
|
||||
|
||||
#### SettingsPageActivity.java
|
||||
- [ ] 返回按钮
|
||||
- [ ] 各个设置项点击
|
||||
- [ ] 退出登录按钮
|
||||
|
||||
### 中优先级(列表项点击)
|
||||
|
||||
#### Adapter类
|
||||
- [ ] ConversationsAdapter - 对话列表项点击
|
||||
- [ ] ConversationMessagesAdapter - 消息项点击(图片、语音)
|
||||
- [ ] CommentAdapter - 评论点赞按钮
|
||||
- [ ] BadgesAdapter - 徽章项点击
|
||||
- [ ] DrawerCardsAdapter - 抽屉卡片点击
|
||||
- [ ] FriendsAdapter - 好友项点击
|
||||
- [ ] NearbyUsersAdapter - 附近用户项点击
|
||||
- [ ] NotificationsAdapter - 通知项点击
|
||||
- [ ] RoomsAdapter - 直播间项点击
|
||||
- [ ] UserWorksAdapter - 作品项点击
|
||||
- [ ] WaterfallRoomsAdapter - 瀑布流直播间项点击
|
||||
|
||||
### 低优先级(不太需要防抖的场景)
|
||||
|
||||
- [ ] 对话框关闭按钮(用户不太会快速重复点击)
|
||||
- [ ] 底部导航栏切换(系统已有防抖机制)
|
||||
- [ ] 长按事件(不需要防抖)
|
||||
|
||||
## 批量替换建议
|
||||
|
||||
可以使用以下正则表达式进行批量查找替换:
|
||||
|
||||
**查找:**
|
||||
```regex
|
||||
\.setOnClickListener\(v -> \{
|
||||
```
|
||||
|
||||
**替换为:**
|
||||
```java
|
||||
.setOnClickListener(new DebounceClickListener() {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
```
|
||||
|
||||
**注意:** 需要手动添加对应的结束括号 `});`
|
||||
|
||||
## 特殊情况处理
|
||||
|
||||
### 1. Lambda表达式单行
|
||||
```java
|
||||
// 之前
|
||||
button.setOnClickListener(v -> finish());
|
||||
|
||||
// 之后
|
||||
button.setOnClickListener(new DebounceClickListener() {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 已有变量引用
|
||||
```java
|
||||
// 之前
|
||||
View.OnClickListener listener = v -> doSomething();
|
||||
button.setOnClickListener(listener);
|
||||
|
||||
// 之后
|
||||
View.OnClickListener listener = new DebounceClickListener() {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
doSomething();
|
||||
}
|
||||
};
|
||||
button.setOnClickListener(listener);
|
||||
```
|
||||
|
||||
### 3. 在Adapter中使用
|
||||
```java
|
||||
// 在ViewHolder的bind方法中
|
||||
public void bind(Item item) {
|
||||
itemView.setOnClickListener(new DebounceClickListener() {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
if (onItemClickListener != null) {
|
||||
onItemClickListener.onItemClick(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
添加防抖后,建议测试以下场景:
|
||||
1. 快速连续点击按钮,确认只触发一次
|
||||
2. 正常点击间隔(>500ms),确认每次都能触发
|
||||
3. 不同防抖时间的按钮,确认各自独立工作
|
||||
4. 列表滚动时点击,确认不会误触发
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要对所有点击都添加防抖**:某些场景(如音乐播放器的快进/快退)不适合防抖
|
||||
2. **防抖时间要合理**:默认500ms适合大多数场景,但某些场景可能需要调整
|
||||
3. **保持一致性**:同类型的操作使用相同的防抖时间
|
||||
4. **测试充分**:添加防抖后要测试各种点击场景
|
||||
|
||||
## 进度追踪
|
||||
|
||||
- ✅ 已完成:WorkDetailActivity.java
|
||||
- 🔄 进行中:其他Activity
|
||||
- ⏳ 待处理:Adapter类
|
||||
|
||||
最后更新:2024-12-24
|
||||
Loading…
Reference in New Issue
Block a user