diff --git a/Log/8-配置上传服务器.md b/Log/8-配置上传服务器.md new file mode 100644 index 00000000..584f0cf5 --- /dev/null +++ b/Log/8-配置上传服务器.md @@ -0,0 +1,59 @@ +安装软件 +# 安装 Node.js 18 +curl -fsSL https://rpm.nodesource.com/setup_18.x | bash - +yum install -y nodejs + +# 验证 +node -v +npm -v + +npm install -g pm2 +pm2 start server.js --name upload-server +pm2 save +pm2 startup + +更改配置 +[root@VM-0-16-opencloudos ~]# # 重写正确的配置 +cat > /www/server/panel/vhost/nginx/1.15.149.240_30005.conf << 'EOF' +server +{ + listen 30005; + listen [::]:30005; + server_name 1.15.149.240_30005; + index index.php index.html index.htm default.php default.htm default.html; + root /www/wwwroot/1.15.149.240_30005; + + #CERT-APPLY-CHECK--START + include /www/server/panel/vhost/nginx/well-known/1.15.149.240_30005.conf; + #CERT-APPLY-CHECK--END + + #ERROR-PAGE-START + error_page 404 /404.html; + #ERROR-PAGE-END + + #PHP-INFO-START + include enable-php-84.conf; + #PHP-INFO-END + + #REWRITE-START + include /www/server/panel/vhost/rewrite/1.15.149.240_30005.conf; + #REWRITE-END + + # 上传API代理 + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 500M; + } + + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + { + return 404; + } + +nginx -s reload/www/wwwlogs/1.15.149.240_30005.error.log;a|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { +nginx: [warn] conflicting server name "1.15.149.240" on 0.0.0.0:20001, ignored +nginx: the configuration file /www/server/nginx/conf/nginx.conf syntax is ok +nginx: configuration file /www/server/nginx/conf/nginx.conf test is successful +nginx: [warn] conflicting server name "1.15.149.240" on 0.0.0.0:20001, ignored \ No newline at end of file diff --git a/Log/指令/2026年1月8日任务.md b/Log/指令/2026年1月8日任务.md new file mode 100644 index 00000000..1d473d79 --- /dev/null +++ b/Log/指令/2026年1月8日任务.md @@ -0,0 +1,46 @@ +我们正在开发直播App的三个新功能,请继续之前的工作: + +【功能1】阅后即焚图片消息 +- 好友私聊中发送阅后即焚图片 +- 查看几秒后自动销毁 +- 需要在Android端和后端实现 + +【功能2】作品上热门 +- 用户付费让作品上热门 +- 热门作品在首页优先展示 +- 需要付费档位和热门队列 + +【功能3】发布动态 + 附近动态 +- 用户发布动态(文字+图片+位置) +- 查看附近的人的动态 +- 基于地理位置发现 + +当前项目结构: +- Android端:android-app/app/src/main/java/com/example/livestreaming/ +- 后端Java:Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/ +- 后端Controller:Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ + +请根据我指定的功能继续开发。 +我们正在开发直播App的三个新功能,请继续之前的工作: + +【功能1】阅后即焚图片消息 +- 好友私聊中发送阅后即焚图片 +- 查看几秒后自动销毁 +- 需要在Android端和后端实现 + +【功能2】作品上热门 +- 用户付费让作品上热门 +- 热门作品在首页优先展示 +- 需要付费档位和热门队列 + +【功能3】发布动态 + 附近动态 +- 用户发布动态(文字+图片+位置) +- 查看附近的人的动态 +- 基于地理位置发现 + +当前项目结构: +- Android端:android-app/app/src/main/java/com/example/livestreaming/ +- 后端Java:Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/ +- 后端Controller:Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ + +请根据我指定的功能继续开发。 diff --git a/Log/1-AI指南.md b/Log/环境/1-AI指南.md similarity index 98% rename from Log/1-AI指南.md rename to Log/环境/1-AI指南.md index 4f1e7c89..a7a1bfaf 100644 --- a/Log/1-AI指南.md +++ b/Log/环境/1-AI指南.md @@ -1,3 +1,8 @@ +# 手动引用 +1. 你明白我的意思吗。有没有不清楚的问题,请先询问我然后进行开发。有歧义要先询问我之后再进行下一步,不能你自己猜测可能的结果 +2. + + # AI工作指南 ## 🚀 快速引用 diff --git a/Log/2-项目功能与部署指南.md b/Log/环境/2-项目功能与部署指南.md similarity index 100% rename from Log/2-项目功能与部署指南.md rename to Log/环境/2-项目功能与部署指南.md diff --git a/Log/3-JDK17环境.md b/Log/环境/3-JDK17环境.md similarity index 100% rename from Log/3-JDK17环境.md rename to Log/环境/3-JDK17环境.md diff --git a/Log/4-Live-streaming启动环境.md b/Log/环境/4-Live-streaming启动环境.md similarity index 100% rename from Log/4-Live-streaming启动环境.md rename to Log/环境/4-Live-streaming启动环境.md diff --git a/Log/5-jar包启动环境.md b/Log/环境/5-jar包启动环境.md similarity index 100% rename from Log/5-jar包启动环境.md rename to Log/环境/5-jar包启动环境.md diff --git a/Log/6-nginx配置.md b/Log/环境/6-nginx配置.md similarity index 100% rename from Log/6-nginx配置.md rename to Log/环境/6-nginx配置.md diff --git a/Log/7-Docker环境配置.md b/Log/环境/7-Docker环境配置.md similarity index 100% rename from Log/7-Docker环境配置.md rename to Log/环境/7-Docker环境配置.md diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml b/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml index f7829dae..7f6a7d48 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml @@ -36,7 +36,7 @@ LIVE_PUBLIC_SRS_HTTP_PORT: 25003 file: upload: server: - url: http://1.15.149.240:30005/upload # 文件上传服务器地址 + url: http://1.15.149.240:30005/api/upload # 文件上传服务器地址 # 配置端口 server: diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/chat/PrivateMessageBurn.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/chat/PrivateMessageBurn.java new file mode 100644 index 00000000..171c5821 --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/model/chat/PrivateMessageBurn.java @@ -0,0 +1,68 @@ +package com.zbkj.common.model.chat; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import javax.persistence.*; +import java.io.Serializable; +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("eb_private_message_burn") +@Entity +@Table(name = "eb_private_message_burn", indexes = { + @Index(name = "uk_message_id", columnList = "message_id", unique = true), + @Index(name = "idx_conversation_id", columnList = "conversation_id"), + @Index(name = "idx_burn_at", columnList = "burn_at") +}) +@ApiModel(value = "PrivateMessageBurn对象", description = "阅后即焚消息状态") +public class PrivateMessageBurn implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "ID") + @TableId(value = "id", type = IdType.AUTO) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "消息ID") + @Column(name = "message_id", nullable = false) + private Long messageId; + + @ApiModelProperty(value = "会话ID") + @Column(name = "conversation_id", nullable = false) + private Long conversationId; + + @ApiModelProperty(value = "销毁秒数") + @Column(name = "burn_seconds") + private Integer burnSeconds; + + @ApiModelProperty(value = "首次查看时间") + @Column(name = "viewed_at") + private Date viewedAt; + + @ApiModelProperty(value = "销毁时间") + @Column(name = "burn_at") + private Date burnAt; + + @ApiModelProperty(value = "是否已销毁") + @Column(name = "burned", columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean burned; + + @ApiModelProperty(value = "创建时间") + @Column(name = "create_time") + private Date createTime; + + @ApiModelProperty(value = "更新时间") + @Column(name = "update_time") + private Date updateTime; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/SendMessageRequest.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/SendMessageRequest.java index 6445f321..ad5195c1 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/SendMessageRequest.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/request/SendMessageRequest.java @@ -28,6 +28,9 @@ public class SendMessageRequest implements Serializable { @ApiModelProperty(value = "语音时长(可选)") private Integer duration; + @ApiModelProperty(value = "阅后即焚秒数(可选,仅messageType=burn_image时使用)") + private Integer burnSeconds; + // 兼容旧字段 public String getMessage() { return content; diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/BurnViewResponse.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/BurnViewResponse.java new file mode 100644 index 00000000..916dac6a --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/BurnViewResponse.java @@ -0,0 +1,32 @@ +package com.zbkj.common.response; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +@Data +@ApiModel(value = "BurnViewResponse对象", description = "阅后即焚图片查看响应") +public class BurnViewResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "消息ID") + private Long messageId; + + @ApiModelProperty(value = "媒体URL(可为空,已销毁或无权限时不返回)") + private String mediaUrl; + + @ApiModelProperty(value = "销毁秒数") + private Integer burnSeconds; + + @ApiModelProperty(value = "首次查看时间戳(毫秒)") + private Long viewedAt; + + @ApiModelProperty(value = "销毁时间戳(毫秒)") + private Long burnAt; + + @ApiModelProperty(value = "是否已销毁") + private Boolean burned; +} diff --git a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/ChatMessageResponse.java b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/ChatMessageResponse.java index 3878b347..98ff5745 100644 --- a/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/ChatMessageResponse.java +++ b/Zhibo/zhibo-h/crmeb-common/src/main/java/com/zbkj/common/response/ChatMessageResponse.java @@ -57,6 +57,18 @@ public class ChatMessageResponse implements Serializable { @ApiModelProperty(value = "语音时长(秒)") private Integer duration; + @ApiModelProperty(value = "阅后即焚秒数(仅messageType=burn_image时)") + private Integer burnSeconds; + + @ApiModelProperty(value = "首次查看时间戳(毫秒,仅messageType=burn_image时)") + private Long viewedAt; + + @ApiModelProperty(value = "销毁时间戳(毫秒,仅messageType=burn_image时)") + private Long burnAt; + + @ApiModelProperty(value = "是否已销毁(仅messageType=burn_image时)") + private Boolean burned; + @ApiModelProperty(value = "是否已读") private Boolean isRead; diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java index 87591c4f..6f8eb1a7 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java @@ -67,14 +67,11 @@ public class CategoryController { @ApiOperation(value = "获取作品分类列表") @GetMapping("/work") public CommonResult> getWorkCategories() { - List categories = categoryService.getList( - new com.zbkj.common.request.CategorySearchRequest() - .setType(CategoryConstants.CATEGORY_TYPE_WORK) - .setStatus(CategoryConstants.CATEGORY_STATUS_NORMAL) - ); + // 暂时使用直播间分类作为作品分类,实现统一分类系统 + List liveCategories = liveRoomCategoryService.getEnabledList(); - List response = categories.stream() - .map(this::toCategoryResponse) + List response = liveCategories.stream() + .map(this::toLiveRoomCategoryResponse) .collect(Collectors.toList()); return CommonResult.success(response); diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java index 7496a302..14ae3881 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/ConversationController.java @@ -2,6 +2,7 @@ package com.zbkj.front.controller; import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.request.SendMessageRequest; +import com.zbkj.common.response.BurnViewResponse; import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ConversationResponse; import com.zbkj.common.result.CommonResult; @@ -157,4 +158,12 @@ public class ConversationController { Integer userId = userService.getUserIdException(); return CommonResult.success(conversationService.recallMessage(id, userId)); } + + @ApiOperation(value = "查看阅后即焚图片(触发计时)") + @ApiImplicitParam(name = "id", value = "消息ID", required = true) + @PostMapping("/messages/{id}/burn/view") + public CommonResult viewBurnImage(@PathVariable Long id) { + Integer userId = userService.getUserIdException(); + return CommonResult.success(conversationService.viewBurnImage(id, userId)); + } } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml index 8b451731..9032c786 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml +++ b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml @@ -36,6 +36,12 @@ LIVE_PUBLIC_SRS_HOST: 1.15.149.240 LIVE_PUBLIC_SRS_RTMP_PORT: 25002 LIVE_PUBLIC_SRS_HTTP_PORT: 25003 +# ============ 文件上传服务器配置 ============ +file: + upload: + server: + url: http://1.15.149.240:30005/api/upload + spring: profiles: # 配置的环境 diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/PrivateMessageBurnDao.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/PrivateMessageBurnDao.java new file mode 100644 index 00000000..c1393d1b --- /dev/null +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/dao/PrivateMessageBurnDao.java @@ -0,0 +1,7 @@ +package com.zbkj.service.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.zbkj.common.model.chat.PrivateMessageBurn; + +public interface PrivateMessageBurnDao extends BaseMapper { +} diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java index 6c6059c9..e95f674c 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/ConversationService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.model.chat.PrivateMessage; import com.zbkj.common.request.SendMessageRequest; +import com.zbkj.common.response.BurnViewResponse; import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ConversationResponse; @@ -78,4 +79,6 @@ public interface ConversationService extends IService { * 发送消息(带详细参数) */ PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl); + + BurnViewResponse viewBurnImage(Long messageId, Integer userId); } diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java index 16459baf..042ef53a 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/ConversationServiceImpl.java @@ -6,12 +6,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.zbkj.common.exception.CrmebException; import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.model.chat.PrivateMessage; +import com.zbkj.common.model.chat.PrivateMessageBurn; import com.zbkj.common.model.user.User; import com.zbkj.common.request.SendMessageRequest; +import com.zbkj.common.response.BurnViewResponse; import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ConversationResponse; import com.zbkj.service.dao.ConversationDao; import com.zbkj.service.dao.PrivateMessageDao; +import com.zbkj.service.dao.PrivateMessageBurnDao; import com.zbkj.service.dao.UserBlacklistDao; import com.zbkj.service.service.ConversationService; import com.zbkj.service.service.OnlineStatusService; @@ -36,6 +39,9 @@ public class ConversationServiceImpl extends ServiceImpl uw = new LambdaUpdateWrapper<>(); @@ -236,6 +271,84 @@ public class ConversationServiceImpl extends ServiceImpl() + .eq(PrivateMessageBurn::getMessageId, messageId) + .last("LIMIT 1")); + + if (burn == null) { + throw new CrmebException("阅后状态不存在"); + } + + Date now = new Date(); + + boolean burned = Boolean.TRUE.equals(burn.getBurned()); + if (!burned && burn.getBurnAt() != null && burn.getBurnAt().getTime() <= now.getTime()) { + burn.setBurned(true); + burn.setUpdateTime(now); + privateMessageBurnDao.updateById(burn); + burned = true; + } + + BurnViewResponse resp = new BurnViewResponse(); + resp.setMessageId(messageId); + resp.setBurnSeconds(burn.getBurnSeconds()); + resp.setViewedAt(burn.getViewedAt() != null ? burn.getViewedAt().getTime() : null); + resp.setBurnAt(burn.getBurnAt() != null ? burn.getBurnAt().getTime() : null); + resp.setBurned(burned); + + if (burned) { + resp.setMediaUrl(null); + return resp; + } + + boolean isSender = message.getSenderId().equals(userId); + boolean isReceiver = message.getReceiverId().equals(userId); + + if (isReceiver) { + if (burn.getViewedAt() == null) { + burn.setViewedAt(now); + long burnAtMs = now.getTime() + (burn.getBurnSeconds() != null ? burn.getBurnSeconds() : 0) * 1000L; + burn.setBurnAt(new Date(burnAtMs)); + burn.setUpdateTime(now); + privateMessageBurnDao.updateById(burn); + + resp.setViewedAt(burn.getViewedAt().getTime()); + resp.setBurnAt(burn.getBurnAt().getTime()); + resp.setMediaUrl(message.getMediaUrl()); + } else { + // 接收方只允许查看一次 + resp.setMediaUrl(null); + } + return resp; + } + + if (isSender) { + resp.setMediaUrl(message.getMediaUrl()); + return resp; + } + + resp.setMediaUrl(null); + return resp; + } + @Override @Transactional(rollbackFor = Exception.class) public Boolean deleteMessage(Long messageId, Integer userId) { @@ -395,6 +508,26 @@ public class ConversationServiceImpl extends ServiceImpl() + .eq(PrivateMessageBurn::getMessageId, messageId) + .last("LIMIT 1")); + if (burn == null) { + return; + } + + response.setBurnSeconds(burn.getBurnSeconds()); + response.setViewedAt(burn.getViewedAt() != null ? burn.getViewedAt().getTime() : null); + response.setBurnAt(burn.getBurnAt() != null ? burn.getBurnAt().getTime() : null); + + boolean burned = Boolean.TRUE.equals(burn.getBurned()); + if (!burned && burn.getBurnAt() != null && burn.getBurnAt().getTime() <= System.currentTimeMillis()) { + burned = true; + } + response.setBurned(burned); + } + /** * 转换消息列表为响应对象列表 */ @@ -425,6 +558,11 @@ public class ConversationServiceImpl extends ServiceImpl implements Wo @Autowired private UserService userService; - @Autowired - private CategoryService categoryService; - @Autowired private LiveRoomCategoryService liveRoomCategoryService; @@ -80,10 +74,10 @@ public class WorksServiceImpl extends ServiceImpl implements Wo throw new CrmebException("作品类型只能是IMAGE或VIDEO"); } - // 验证分类是否存在(如果提供了分类ID且不为0) - if (request.getCategoryId() != null && request.getCategoryId() > 0) { - LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); - if (category == null || category.getStatus() != 1) { + // 验证分类是否存在(如果提供了分类ID) + if (request.getCategoryId() != null) { + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); + if (category == null || category.getStatus() == null || category.getStatus() != 1) { throw new CrmebException("分类不存在或已禁用"); } } @@ -154,6 +148,21 @@ public class WorksServiceImpl extends ServiceImpl implements Wo @Override @Transactional(rollbackFor = Exception.class) public Boolean updateWorks(WorksRequest request, Integer userId) { + log.info("=== 开始更新作品 ==="); + log.info("作品ID: {}", request.getId()); + log.info("用户ID: {}", userId); + log.info("标题: {}", request.getTitle()); + log.info("描述: {}", request.getDescription()); + log.info("类型: {}", request.getType()); + log.info("封面URL: {}", request.getCoverUrl()); + log.info("视频URL: {}", request.getVideoUrl()); + log.info("图片URLs: {}", request.getImageUrls() != null ? request.getImageUrls().size() + " 张图片" : "无图片"); + if (request.getImageUrls() != null) { + for (int i = 0; i < request.getImageUrls().size(); i++) { + log.info(" 图片 {}: {}", i+1, request.getImageUrls().get(i)); + } + } + if (request.getId() == null) { throw new CrmebException("作品ID不能为空"); } @@ -163,16 +172,23 @@ public class WorksServiceImpl extends ServiceImpl implements Wo if (works == null || works.getIsDeleted() == 1) { throw new CrmebException("作品不存在"); } + + log.info("原作品信息:"); + log.info(" 原标题: {}", works.getTitle()); + log.info(" 原描述: {}", works.getDescription()); + log.info(" 原封面: {}", works.getCoverImage()); + log.info(" 原图片: {}", works.getImages()); + log.info(" 原视频: {}", works.getVideoUrl()); - // 管理端(userId=1)可以编辑所有作品,普通用户只能编辑自己的作品 - if (userId != 1 && !works.getUid().equals(userId)) { + // 验证是否是作品作者 + if (!works.getUid().equals(userId)) { throw new CrmebException("无权限编辑此作品"); } - // 验证分类是否存在(如果提供了分类ID且不为0) - if (request.getCategoryId() != null && request.getCategoryId() > 0) { - LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); - if (category == null || category.getStatus() != 1) { + // 验证分类是否存在(如果提供了分类ID) + if (request.getCategoryId() != null) { + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); + if (category == null || category.getStatus() == null || category.getStatus() != 1) { throw new CrmebException("分类不存在或已禁用"); } } @@ -193,12 +209,64 @@ public class WorksServiceImpl extends ServiceImpl implements Wo if (request.getStatus() != null) { works.setStatus(request.getStatus()); } + + // 更新媒体资源 + if (request.getCoverUrl() != null) { + works.setCoverImage(request.getCoverUrl()); + } + + // 处理图片列表 + if (request.getImageUrls() != null) { + if (request.getImageUrls().isEmpty()) { + works.setImages(null); + } else { + works.setImages(String.join(",", request.getImageUrls())); + } + } + + // 处理视频URL + if (request.getVideoUrl() != null) { + works.setVideoUrl(request.getVideoUrl()); + } + + // 更新作品类型(如果提供) + if (request.getType() != null) { + works.setType(request.getType().toUpperCase()); + } + + // 更新位置信息 + if (request.getLocation() != null) { + works.setLocation(request.getLocation()); + } + + // 更新可见范围 + if (request.getVisibility() != null) { + String visibility = request.getVisibility().toUpperCase(); + if (visibility.equals("PUBLIC") || visibility.equals("FRIENDS") || visibility.equals("PRIVATE")) { + works.setVisibility(visibility); + } + } + + // 更新评论设置 + if (request.getCommentSetting() != null) { + String commentSetting = request.getCommentSetting().toUpperCase(); + if (commentSetting.equals("ALL") || commentSetting.equals("FRIENDS") || commentSetting.equals("DISABLED")) { + works.setCommentSetting(commentSetting); + } + } boolean updated = updateById(works); if (!updated) { throw new CrmebException("更新作品失败"); } + log.info("=== 作品更新完成 ==="); + log.info("更新后作品信息:"); + log.info(" 新标题: {}", works.getTitle()); + log.info(" 新描述: {}", works.getDescription()); + log.info(" 新封面: {}", works.getCoverImage()); + log.info(" 新图片: {}", works.getImages()); + log.info(" 新视频: {}", works.getVideoUrl()); log.info("用户{}更新作品成功,作品ID:{}", userId, works.getId()); return true; } @@ -212,8 +280,8 @@ public class WorksServiceImpl extends ServiceImpl implements Wo throw new CrmebException("作品不存在"); } - // 管理端(userId=1)可以删除所有作品,普通用户只能删除自己的作品 - if (userId != 1 && !works.getUid().equals(userId)) { + // 验证是否是作品作者 + if (!works.getUid().equals(userId)) { throw new CrmebException("无权限删除此作品"); } @@ -236,7 +304,7 @@ public class WorksServiceImpl extends ServiceImpl implements Wo public Boolean updateWorksStatus(Long worksId, Integer status) { // 验证状态值 if (status != 0 && status != 1) { - throw new CrmebException("状态值只能是0(下架)或1(上架)"); + throw new CrmebException("状态值无效,只能是0(下架)或1(上架)"); } // 查询作品 @@ -319,8 +387,10 @@ public class WorksServiceImpl extends ServiceImpl implements Wo // 状态筛选 if (request.getStatus() != null) { queryWrapper.eq(Works::getStatus, request.getStatus()); + } else { + // 默认只查询正常状态的作品 + queryWrapper.eq(Works::getStatus, 1); } - // 管理端不过滤状态,可以看到所有作品(包括已下架的) // 排序 String orderBy = request.getOrderBy(); @@ -452,7 +522,7 @@ public class WorksServiceImpl extends ServiceImpl implements Wo // 获取分类信息 if (works.getCategoryId() != null) { - LiveRoomCategory category = liveRoomCategoryService.getById(works.getCategoryId()); + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(works.getCategoryId()); if (category != null) { response.setCategoryName(category.getName()); } diff --git a/Zhibo/zhibo-h/sql/create_private_message_burn_table.sql b/Zhibo/zhibo-h/sql/create_private_message_burn_table.sql new file mode 100644 index 00000000..911044d9 --- /dev/null +++ b/Zhibo/zhibo-h/sql/create_private_message_burn_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `eb_private_message_burn` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', + `message_id` BIGINT(20) NOT NULL COMMENT '消息ID', + `conversation_id` BIGINT(20) NOT NULL COMMENT '会话ID', + `burn_seconds` INT(11) DEFAULT NULL COMMENT '销毁秒数', + `viewed_at` DATETIME DEFAULT NULL COMMENT '首次查看时间', + `burn_at` DATETIME DEFAULT NULL COMMENT '销毁时间', + `burned` TINYINT(1) DEFAULT 0 COMMENT '是否已销毁', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_message_id` (`message_id`), + KEY `idx_conversation_id` (`conversation_id`), + KEY `idx_burn_at` (`burn_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阅后即焚消息状态表'; diff --git a/Zhibo/zhibo-h/sql/init_live_room_categories.sql b/Zhibo/zhibo-h/sql/init_live_room_categories.sql new file mode 100644 index 00000000..788753b2 --- /dev/null +++ b/Zhibo/zhibo-h/sql/init_live_room_categories.sql @@ -0,0 +1,18 @@ +-- 初始化直播间分类数据 +-- 确保eb_live_room_category表有基础的分类数据 + +USE zhibo; + +-- 插入基础的直播间分类数据(如果不存在) +INSERT IGNORE INTO eb_live_room_category (id, name, sort, status, create_time, update_time) VALUES +(1, '娱乐', 1, 1, NOW(), NOW()), +(2, '游戏', 2, 1, NOW(), NOW()), +(3, '音乐', 3, 1, NOW(), NOW()), +(4, '户外', 4, 1, NOW(), NOW()), +(5, '美食', 5, 1, NOW(), NOW()), +(6, '体育', 6, 1, NOW(), NOW()), +(7, '教育', 7, 1, NOW(), NOW()), +(8, '科技', 8, 1, NOW(), NOW()); + +-- 查看插入结果 +SELECT id, name, status FROM eb_live_room_category ORDER BY sort; \ No newline at end of file diff --git a/Zhibo/zhibo-h/sql/optimize_works_category.sql b/Zhibo/zhibo-h/sql/optimize_works_category.sql new file mode 100644 index 00000000..22690ef7 --- /dev/null +++ b/Zhibo/zhibo-h/sql/optimize_works_category.sql @@ -0,0 +1,12 @@ +-- 作品分类功能优化 +-- 作品表已经有 category_id 字段支持单分类选择 +-- 确保索引存在以提高查询性能 + +USE zhibo; + +-- 确保 category_id 字段存在并有正确的索引 +-- 如果索引不存在,创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_category_id ON eb_works(category_id); + +-- 更新表注释 +ALTER TABLE eb_works COMMENT = '作品表 - 支持单分类选择'; \ No newline at end of file diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 786795b8..2476d271 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -101,6 +101,16 @@ android:name="com.example.livestreaming.LikedRoomsActivity" android:exported="false" /> + + + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java deleted file mode 100644 index c35287f5..00000000 --- a/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.example.livestreaming; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -/** - * 频道管理适配器 - * 用于显示我的频道和推荐频道 - */ -public class ChannelManagerAdapter extends RecyclerView.Adapter { - - public interface OnChannelClickListener { - void onChannelClick(ChannelItem item, int position); - void onChannelDelete(ChannelItem item, int position); - void onChannelAdd(ChannelItem item, int position); - } - - private final List items = new ArrayList<>(); - private OnChannelClickListener listener; - private boolean isEditMode = false; - private boolean isRecommendMode = false; // 是否是推荐频道模式 - private int fixedCount = 4; // 前4个固定不能删除 - - public void setOnChannelClickListener(OnChannelClickListener listener) { - this.listener = listener; - } - - public void setEditMode(boolean editMode) { - this.isEditMode = editMode; - notifyDataSetChanged(); - } - - public void setRecommendMode(boolean recommendMode) { - this.isRecommendMode = recommendMode; - } - - public void setFixedCount(int count) { - this.fixedCount = count; - } - - public void submitList(List newItems) { - items.clear(); - if (newItems != null) { - items.addAll(newItems); - } - notifyDataSetChanged(); - } - - public List getItems() { - return new ArrayList<>(items); - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_channel_tag, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - ChannelItem item = items.get(position); - holder.bind(item, position); - } - - @Override - public int getItemCount() { - return items.size(); - } - - class ViewHolder extends RecyclerView.ViewHolder { - private final TextView channelName; - private final ImageView fixedIcon; - private final ImageView deleteIcon; - private final ImageView addIcon; - - ViewHolder(@NonNull View itemView) { - super(itemView); - channelName = itemView.findViewById(R.id.channelName); - fixedIcon = itemView.findViewById(R.id.fixedIcon); - deleteIcon = itemView.findViewById(R.id.deleteIcon); - addIcon = itemView.findViewById(R.id.addIcon); - } - - void bind(ChannelItem item, int position) { - channelName.setText(item.getName()); - - // 重置所有图标状态 - fixedIcon.setVisibility(View.GONE); - deleteIcon.setVisibility(View.GONE); - addIcon.setVisibility(View.GONE); - - if (isRecommendMode) { - // 推荐频道模式:显示添加图标 - addIcon.setVisibility(View.VISIBLE); - itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelAdd(item, position); - } - }); - } else { - // 我的频道模式 - boolean isFixed = position < fixedCount; - - if (isEditMode) { - // 编辑模式 - if (isFixed) { - fixedIcon.setVisibility(View.VISIBLE); - } else { - deleteIcon.setVisibility(View.VISIBLE); - deleteIcon.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelDelete(item, position); - } - }); - } - } - - itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelClick(item, position); - } - }); - } - } - } - - /** - * 频道项数据类 - */ - public static class ChannelItem { - private final String id; - private final String name; - private boolean isFixed; - - public ChannelItem(String id, String name) { - this.id = id; - this.name = name; - this.isFixed = false; - } - - public ChannelItem(String id, String name, boolean isFixed) { - this.id = id; - this.name = name; - this.isFixed = isFixed; - } - - public String getId() { return id; } - public String getName() { return name; } - public boolean isFixed() { return isFixed; } - public void setFixed(boolean fixed) { this.isFixed = fixed; } - } -} diff --git a/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java index e4e81ea2..175265e2 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java @@ -71,40 +71,72 @@ public class ChannelTagAdapter extends ListAdapter { - if (listener != null) { - if (isRecommendMode) { - listener.onTagAddClick(tag, position); + + try { + // 显示文本 + if (isRecommendMode) { + tagText.setText("+ " + tag.getName()); + deleteIcon.setVisibility(View.GONE); + } else { + tagText.setText(tag.getName()); + // 我的频道模式下,除了"推荐"外都可以删除 + if (position == 0 && "推荐".equals(tag.getName())) { + deleteIcon.setVisibility(View.GONE); } else { - listener.onTagClick(tag, position); + deleteIcon.setVisibility(View.VISIBLE); } } - }); + + // 选中状态样式 - 更明显的标签样式 + boolean isSelected = position == selectedPosition; + if (isSelected) { + // 选中状态:蓝色背景,白色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected); + tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.white, null)); + } else { + if (isRecommendMode) { + // 推荐频道:虚线边框,紫色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_recommend); + tagText.setTextColor(itemView.getContext().getResources().getColor(R.color.purple_500, null)); + } else { + // 我的频道:实线边框,深灰色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_normal); + tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null)); + } + } + + // 标签点击事件 + tagText.setOnClickListener(v -> { + if (listener != null) { + if (isRecommendMode) { + listener.onTagAddClick(tag, position); + } else { + listener.onTagClick(tag, position); + } + } + }); + + // 删除按钮点击事件 + deleteIcon.setOnClickListener(v -> { + if (listener != null && !isRecommendMode) { + listener.onTagClick(tag, position); + } + }); + } catch (Exception e) { + android.util.Log.e("ChannelTagAdapter", "绑定标签数据失败", e); + } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 2834582d..9a179115 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -17,6 +17,7 @@ import android.speech.SpeechRecognizer; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.view.LayoutInflater; import android.view.View; import android.widget.ArrayAdapter; import android.widget.TextView; @@ -44,6 +45,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputLayout; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.CommunityResponse; import com.example.livestreaming.net.ConversationResponse; import com.example.livestreaming.net.CreateRoomRequest; @@ -201,7 +203,8 @@ public class MainActivity extends AppCompatActivity { // 隐藏粉丝和获赞菜单项 // items.add(new DrawerCardItem(DrawerCardItem.ACTION_FANS, "粉丝", "关注你的人", R.drawable.ic_people_24)); // items.add(new DrawerCardItem(DrawerCardItem.ACTION_LIKES, "获赞", "收到的点赞", R.drawable.ic_heart_24)); - items.add(new DrawerCardItem(DrawerCardItem.ACTION_HISTORY, "观看历史", "最近看过的直播", R.drawable.ic_grid_24)); + // 隐藏观看历史菜单项 + // items.add(new DrawerCardItem(DrawerCardItem.ACTION_HISTORY, "观看历史", "最近看过的直播", R.drawable.ic_grid_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_SEARCH, "搜索", "找主播/房间/标签", R.drawable.ic_search_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_SETTINGS, "设置", "账号、隐私、通知", R.drawable.ic_menu_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_HELP, "帮助与反馈", "常见问题与建议", R.drawable.ic_chat_24)); @@ -406,6 +409,17 @@ public class MainActivity extends AppCompatActivity { }); } + // 设置分类展开按钮点击事件 + View btnExpandCategories = findViewById(R.id.btnExpandCategories); + if (btnExpandCategories != null) { + btnExpandCategories.setOnClickListener(new DebounceClickListener() { + @Override + public void onDebouncedClick(View v) { + showCategoryManagementDialog(); + } + }); + } + // 设置通知图标点击事件(如果存在) try { View notificationIcon = findViewById(R.id.notificationIcon); @@ -1759,6 +1773,9 @@ public class MainActivity extends AppCompatActivity { private void loadCategoriesFromBackend() { Log.d(TAG, "loadCategoriesFromBackend() 开始加载直播间分类"); + // 先从本地加载我的频道配置 + loadMyChannelsFromPrefs(); + // 调用后端接口获取直播间分类列表 ApiClient.getService(getApplicationContext()).getLiveRoomCategories() .enqueue(new Callback>>() { @@ -1775,8 +1792,11 @@ public class MainActivity extends AppCompatActivity { if (categories != null && !categories.isEmpty()) { Log.d(TAG, "loadCategoriesFromBackend() 成功获取 " + categories.size() + " 个分类"); - // 更新分类标签 - updateCategoryTabs(categories); + // 缓存后端分类数据 + allBackendCategories.clear(); + allBackendCategories.addAll(categories); + // 使用我的频道配置更新分类标签 + updateCategoryTabsFromMyChannels(); } else { Log.w(TAG, "loadCategoriesFromBackend() 未获取到分类数据,使用默认分类"); // 使用默认分类 @@ -1831,32 +1851,14 @@ public class MainActivity extends AppCompatActivity { private void useDefaultCategories() { if (binding == null || binding.categoryTabs == null) return; + // 如果我的频道配置为空,初始化默认配置 + if (myChannels.isEmpty()) { + initDefaultMyChannels(); + } + runOnUiThread(() -> { - // 清空现有标签 - binding.categoryTabs.removeAllTabs(); - - // 使用后端分类(如果已加载) - if (!allBackendCategories.isEmpty()) { - // 添加"推荐"作为第一个标签 - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐")); - - // 添加后端分类 - for (com.example.livestreaming.net.CategoryResponse cat : allBackendCategories) { - if (cat != null && cat.getName() != null) { - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(cat.getName())); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("娱乐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("游戏")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("音乐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("户外")); - } - - // 恢复上次选中的分类 - restoreCategoryTabSelection(); + // 使用我的频道配置更新分类标签 + updateCategoryTabsFromMyChannels(); Log.d(TAG, "useDefaultCategories() 使用默认分类,共 " + binding.categoryTabs.getTabCount() + " 个标签"); }); @@ -3540,246 +3542,6 @@ public class MainActivity extends AppCompatActivity { }); } - /** - * 显示频道管理底部弹窗 - */ - private void showChannelManagerDialog() { - // 创建底部弹窗 - com.google.android.material.bottomsheet.BottomSheetDialog dialog = - new com.google.android.material.bottomsheet.BottomSheetDialog(this); - View dialogView = getLayoutInflater().inflate(R.layout.bottom_sheet_channel_manager, null); - dialog.setContentView(dialogView); - - // 获取视图引用 - RecyclerView myChannelRecycler = dialogView.findViewById(R.id.myChannelsRecycler); - RecyclerView recommendChannelRecycler = dialogView.findViewById(R.id.recommendChannelsRecycler); - TextView btnEditChannel = dialogView.findViewById(R.id.btnEditChannels); - View btnCloseChannelManager = dialogView.findViewById(R.id.btnCloseChannelManager); - - // 初始化我的频道适配器 - ChannelManagerAdapter myAdapter = new ChannelManagerAdapter(); - myAdapter.setFixedCount(4); // 前4个固定 - myAdapter.setRecommendMode(false); - - // 初始化推荐频道适配器 - ChannelManagerAdapter recommendAdapter = new ChannelManagerAdapter(); - recommendAdapter.setRecommendMode(true); - - // 设置布局管理器 - 使用FlexboxLayoutManager或GridLayoutManager - myChannelRecycler.setLayoutManager(new androidx.recyclerview.widget.GridLayoutManager(this, 4)); - recommendChannelRecycler.setLayoutManager(new androidx.recyclerview.widget.GridLayoutManager(this, 4)); - - myChannelRecycler.setAdapter(myAdapter); - recommendChannelRecycler.setAdapter(recommendAdapter); - - // 加载我的频道数据(从SharedPreferences或默认值) - List myChannelList = loadMyChannels(); - myAdapter.submitList(myChannelList); - - // 加载推荐频道数据 - List recommendList = loadRecommendChannels(myChannelList); - recommendAdapter.submitList(recommendList); - - // 设置我的频道点击事件 - myAdapter.setOnChannelClickListener(new ChannelManagerAdapter.OnChannelClickListener() { - @Override - public void onChannelClick(ChannelManagerAdapter.ChannelItem item, int position) { - // 点击频道,切换到该分类 - dialog.dismiss(); - selectCategoryTab(item.getName()); - } - - @Override - public void onChannelDelete(ChannelManagerAdapter.ChannelItem item, int position) { - // 删除频道 - List currentList = myAdapter.getItems(); - currentList.remove(position); - myAdapter.submitList(new ArrayList<>(currentList)); - saveMyChannels(currentList); - // 更新推荐列表 - recommendAdapter.submitList(loadRecommendChannels(currentList)); - } - - @Override - public void onChannelAdd(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - }); - - // 设置推荐频道点击事件 - recommendAdapter.setOnChannelClickListener(new ChannelManagerAdapter.OnChannelClickListener() { - @Override - public void onChannelClick(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - - @Override - public void onChannelDelete(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - - @Override - public void onChannelAdd(ChannelManagerAdapter.ChannelItem item, int position) { - // 添加到我的频道 - List currentList = myAdapter.getItems(); - currentList.add(new ChannelManagerAdapter.ChannelItem(item.getId(), item.getName())); - myAdapter.submitList(new ArrayList<>(currentList)); - saveMyChannels(currentList); - // 更新推荐列表 - recommendAdapter.submitList(loadRecommendChannels(currentList)); - } - }); - - // 编辑按钮点击事件 - final boolean[] isEditMode = {false}; - if (btnEditChannel != null) { - btnEditChannel.setOnClickListener(v -> { - isEditMode[0] = !isEditMode[0]; - myAdapter.setEditMode(isEditMode[0]); - btnEditChannel.setText(isEditMode[0] ? "完成" : "编辑"); - }); - } - - // 关闭按钮点击事件 - if (btnCloseChannelManager != null) { - btnCloseChannelManager.setOnClickListener(v -> dialog.dismiss()); - } - - dialog.show(); - } - - /** - * 加载我的频道列表 - */ - private List loadMyChannels() { - List channels = new ArrayList<>(); - - // 从SharedPreferences加载 - android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); - - // 检查是否需要迁移到新版本(使用后端分类) - int savedVersion = prefs.getInt("channel_version", 0); - if (savedVersion < 2) { - // 旧版本数据,清除并使用后端分类 - prefs.edit() - .remove("my_channels") - .putInt("channel_version", 2) - .apply(); - Log.d(TAG, "loadMyChannels() 清除旧版本频道数据,使用后端分类"); - } - - String savedChannels = prefs.getString("my_channels", null); - - if (savedChannels != null && !savedChannels.isEmpty()) { - String[] channelNames = savedChannels.split(","); - for (int i = 0; i < channelNames.length; i++) { - channels.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(i), channelNames[i], i < 4)); - } - } else { - // 使用后端分类作为默认频道(如果有的话) - if (!allBackendCategories.isEmpty()) { - // 添加"推荐"作为第一个固定频道 - channels.add(new ChannelManagerAdapter.ChannelItem("0", "推荐", true)); - // 添加后端分类(最多取前4个作为默认) - int count = Math.min(allBackendCategories.size(), 4); - for (int i = 0; i < count; i++) { - com.example.livestreaming.net.CategoryResponse cat = allBackendCategories.get(i); - if (cat != null && cat.getName() != null) { - channels.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(cat.getId()), cat.getName(), true)); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - channels.add(new ChannelManagerAdapter.ChannelItem("0", "推荐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("1", "娱乐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("2", "游戏", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("3", "音乐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("4", "户外", true)); - } - } - - return channels; - } - - /** - * 保存我的频道列表 - */ - private void saveMyChannels(List channels) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < channels.size(); i++) { - if (i > 0) sb.append(","); - sb.append(channels.get(i).getName()); - } - - android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); - prefs.edit().putString("my_channels", sb.toString()).apply(); - - // 更新分类标签 - updateCategoryTabsFromChannels(channels); - } - - /** - * 加载推荐频道列表(排除已添加的) - */ - private List loadRecommendChannels(List myChannels) { - // 获取已添加的频道名称 - java.util.Set addedNames = new java.util.HashSet<>(); - for (ChannelManagerAdapter.ChannelItem item : myChannels) { - addedNames.add(item.getName()); - } - - List recommendList = new ArrayList<>(); - - // 优先使用后端分类 - if (!allBackendCategories.isEmpty()) { - for (com.example.livestreaming.net.CategoryResponse cat : allBackendCategories) { - if (cat != null && cat.getName() != null && !addedNames.contains(cat.getName())) { - recommendList.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(cat.getId()), cat.getName())); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - String[] defaultChannels = {"娱乐", "游戏", "音乐", "户外", "聊天"}; - for (int i = 0; i < defaultChannels.length; i++) { - if (!addedNames.contains(defaultChannels[i])) { - recommendList.add(new ChannelManagerAdapter.ChannelItem(String.valueOf(i), defaultChannels[i])); - } - } - } - - return recommendList; - } - - /** - * 根据频道列表更新分类标签 - */ - private void updateCategoryTabsFromChannels(List channels) { - if (binding.categoryTabs == null) return; - - binding.categoryTabs.removeAllTabs(); - for (ChannelManagerAdapter.ChannelItem channel : channels) { - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName())); - } - } - - /** - * 选中指定分类标签 - */ - private void selectCategoryTab(String categoryName) { - if (binding.categoryTabs == null) return; - - for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) { - TabLayout.Tab tab = binding.categoryTabs.getTabAt(i); - if (tab != null && categoryName.equals(tab.getText())) { - tab.select(); - break; - } - } - } - /** * 打开应用设置页面,引导用户手动开启权限 */ @@ -3801,4 +3563,378 @@ public class MainActivity extends AppCompatActivity { } } } -} + + /** + * 显示分类管理对话框 + */ + private void showCategoryManagementDialog() { + // 创建底部弹出面板 + com.google.android.material.bottomsheet.BottomSheetDialog bottomSheetDialog = + new com.google.android.material.bottomsheet.BottomSheetDialog(this); + View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_category_management, null); + bottomSheetDialog.setContentView(dialogView); + + // 初始化分类管理界面 + setupCategoryManagementDialog(dialogView, bottomSheetDialog); + + bottomSheetDialog.show(); + } + + /** + * 设置分类管理对话框 + */ + private void setupCategoryManagementDialog(View dialogView, com.google.android.material.bottomsheet.BottomSheetDialog dialog) { + try { + if (dialogView == null || dialog == null) { + Log.e(TAG, "对话框视图或对话框对象为null"); + return; + } + + // 我的频道 + RecyclerView myChannelsRecyclerView = dialogView.findViewById(R.id.myChannelsRecyclerView); + TextView addChannelText = dialogView.findViewById(R.id.addChannelText); + + // 推荐频道 + RecyclerView recommendChannelsRecyclerView = dialogView.findViewById(R.id.recommendChannelsRecyclerView); + TextView recommendTitle = dialogView.findViewById(R.id.recommendTitle); + + // 完成按钮 + TextView completeButton = dialogView.findViewById(R.id.completeButton); + + if (myChannelsRecyclerView == null || recommendChannelsRecyclerView == null || completeButton == null) { + Log.e(TAG, "对话框中的关键视图为null"); + return; + } + + // 设置我的频道列表 + myChannelAdapter = new ChannelTagAdapter(); + myChannelAdapter.setRecommendMode(false); + myChannelAdapter.setOnTagClickListener(new ChannelTagAdapter.OnTagClickListener() { + @Override + public void onTagClick(ChannelTagAdapter.ChannelTag tag, int position) { + // 移除频道(除了"推荐") + try { + if (position == 0 && "推荐".equals(tag.getName())) { + Toast.makeText(MainActivity.this, "推荐频道不能移除", Toast.LENGTH_SHORT).show(); + return; + } + + if (myChannels.size() > 1 && position >= 0 && position < myChannels.size()) { + ChannelTagAdapter.ChannelTag removed = myChannels.remove(position); + if (removed != null) { + // 添加到推荐频道 + recommendChannels.add(removed); + + // 更新适配器 + if (myChannelAdapter != null) { + myChannelAdapter.submitList(new ArrayList<>(myChannels)); + } + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + + Toast.makeText(MainActivity.this, "已移除 " + removed.getName(), Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(MainActivity.this, "至少需要保留一个频道", Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + Log.e(TAG, "移除频道失败", e); + Toast.makeText(MainActivity.this, "移除频道失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onTagAddClick(ChannelTagAdapter.ChannelTag tag, int position) { + // 不处理 + } + }); + + myChannelsRecyclerView.setLayoutManager(new GridLayoutManager(this, 4)); + myChannelsRecyclerView.setAdapter(myChannelAdapter); + + // 设置推荐频道列表 + recommendChannelAdapter = new ChannelTagAdapter(); + recommendChannelAdapter.setRecommendMode(true); + recommendChannelAdapter.setOnTagClickListener(new ChannelTagAdapter.OnTagClickListener() { + @Override + public void onTagClick(ChannelTagAdapter.ChannelTag tag, int position) { + // 不处理 + } + + @Override + public void onTagAddClick(ChannelTagAdapter.ChannelTag tag, int position) { + // 添加频道到我的频道 + try { + if (position >= 0 && position < recommendChannels.size()) { + ChannelTagAdapter.ChannelTag removed = recommendChannels.remove(position); + if (removed != null) { + // 添加到我的频道 + myChannels.add(removed); + + // 更新适配器 + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + if (myChannelAdapter != null) { + myChannelAdapter.submitList(new ArrayList<>(myChannels)); + } + + Toast.makeText(MainActivity.this, "已添加 " + removed.getName(), Toast.LENGTH_SHORT).show(); + } + } + } catch (Exception e) { + Log.e(TAG, "添加频道失败", e); + Toast.makeText(MainActivity.this, "添加频道失败", Toast.LENGTH_SHORT).show(); + } + } + }); + + recommendChannelsRecyclerView.setLayoutManager(new GridLayoutManager(this, 4)); + recommendChannelsRecyclerView.setAdapter(recommendChannelAdapter); + + // 完成按钮点击事件 + completeButton.setOnClickListener(v -> { + // 保存我的频道配置到本地 + saveMyChannelsToPrefs(); + // 更新分类标签 + updateCategoryTabsFromMyChannels(); + dialog.dismiss(); + Toast.makeText(MainActivity.this, "频道设置已保存", Toast.LENGTH_SHORT).show(); + }); + + // 加载分类数据 + loadCategoriesForDialog(); + + } catch (Exception e) { + Log.e(TAG, "设置分类管理对话框失败", e); + Toast.makeText(this, "初始化对话框失败", Toast.LENGTH_SHORT).show(); + if (dialog != null) { + dialog.dismiss(); + } + } + } + + /** + * 加载分类数据用于对话框 + */ + private void loadCategoriesForDialog() { + // 先从本地加载我的频道配置 + loadMyChannelsFromPrefs(); + + ApiService apiService = ApiClient.getService(this); + + // 加载直播间分类 + Call>> call = + apiService.getLiveRoomCategories(); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + try { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List categories = apiResponse.getData(); + + // 在主线程中更新UI + runOnUiThread(() -> { + try { + // 清空推荐频道 + recommendChannels.clear(); + + // 获取我的频道中已有的分类ID + Set myChannelIds = new HashSet<>(); + for (ChannelTagAdapter.ChannelTag myChannel : myChannels) { + if (myChannel != null) { + myChannelIds.add(myChannel.getId()); + } + } + + // 将不在我的频道中的分类添加到推荐频道 + for (com.example.livestreaming.net.CategoryResponse category : categories) { + if (category != null && category.getName() != null && + !myChannelIds.contains(category.getId())) { + recommendChannels.add(new ChannelTagAdapter.ChannelTag(category.getId(), category.getName())); + } + } + + // 更新适配器 + if (myChannelAdapter != null) { + myChannelAdapter.submitList(new ArrayList<>(myChannels)); + } + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + } catch (Exception e) { + Log.e(TAG, "更新推荐频道失败", e); + initDefaultRecommendChannels(); + } + }); + } else { + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } else { + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } catch (Exception e) { + Log.e(TAG, "处理分类数据失败", e); + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载分类失败", t); + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + }); + } + + /** + * 初始化默认的推荐频道 + */ + private void initDefaultRecommendChannels() { + try { + recommendChannels.clear(); + + // 获取我的频道中已有的分类名称 + Set myChannelNames = new HashSet<>(); + for (ChannelTagAdapter.ChannelTag myChannel : myChannels) { + if (myChannel != null && myChannel.getName() != null) { + myChannelNames.add(myChannel.getName()); + } + } + + // 添加一些默认的推荐频道(不在我的频道中的) + String[] defaultChannels = {"游戏", "舞蹈", "二次元", "体育", "户外", "才艺", "美食"}; + int id = 100; // 使用较大的ID避免冲突 + for (String channelName : defaultChannels) { + if (channelName != null && !myChannelNames.contains(channelName)) { + recommendChannels.add(new ChannelTagAdapter.ChannelTag(id++, channelName)); + } + } + + // 更新适配器 + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + } catch (Exception e) { + Log.e(TAG, "初始化默认推荐频道失败", e); + } + } + + /** + * 从我的频道更新分类标签 + */ + private void updateCategoryTabsFromMyChannels() { + try { + if (binding == null || binding.categoryTabs == null) { + Log.w(TAG, "binding或categoryTabs为null,无法更新分类标签"); + return; + } + + binding.categoryTabs.removeAllTabs(); + + // 添加我的频道到分类标签 + for (ChannelTagAdapter.ChannelTag channel : myChannels) { + if (channel != null && channel.getName() != null) { + binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName())); + } + } + + // 选中第一个标签 + if (myChannels.size() > 0 && binding.categoryTabs.getTabCount() > 0) { + ChannelTagAdapter.ChannelTag firstChannel = myChannels.get(0); + if (firstChannel != null && firstChannel.getName() != null) { + currentCategory = firstChannel.getName(); + TabLayout.Tab firstTab = binding.categoryTabs.getTabAt(0); + if (firstTab != null) { + firstTab.select(); + } + } + } + } catch (Exception e) { + Log.e(TAG, "更新分类标签失败", e); + } + } + + /** + * 保存我的频道配置到本地 + */ + private void saveMyChannelsToPrefs() { + android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); + android.content.SharedPreferences.Editor editor = prefs.edit(); + + // 将我的频道列表转换为JSON字符串保存 + StringBuilder channelsJson = new StringBuilder(); + channelsJson.append("["); + for (int i = 0; i < myChannels.size(); i++) { + ChannelTagAdapter.ChannelTag channel = myChannels.get(i); + if (i > 0) channelsJson.append(","); + channelsJson.append("{\"id\":").append(channel.getId()) + .append(",\"name\":\"").append(channel.getName()).append("\"}"); + } + channelsJson.append("]"); + + editor.putString("my_channels", channelsJson.toString()); + editor.apply(); + + Log.d(TAG, "保存我的频道配置: " + channelsJson.toString()); + } + + /** + * 从本地加载我的频道配置 + */ + private void loadMyChannelsFromPrefs() { + android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); + String channelsJson = prefs.getString("my_channels", ""); + + if (!channelsJson.isEmpty()) { + try { + // 简单解析JSON字符串 + myChannels.clear(); + channelsJson = channelsJson.trim(); + if (channelsJson.startsWith("[") && channelsJson.endsWith("]")) { + channelsJson = channelsJson.substring(1, channelsJson.length() - 1); + if (!channelsJson.isEmpty()) { + String[] items = channelsJson.split("\\},\\{"); + for (String item : items) { + item = item.replace("{", "").replace("}", ""); + String[] parts = item.split(","); + if (parts.length >= 2) { + int id = Integer.parseInt(parts[0].split(":")[1]); + String name = parts[1].split(":")[1].replace("\"", ""); + myChannels.add(new ChannelTagAdapter.ChannelTag(id, name)); + } + } + } + } + Log.d(TAG, "加载我的频道配置: " + myChannels.size() + " 个频道"); + } catch (Exception e) { + Log.e(TAG, "解析我的频道配置失败", e); + // 如果解析失败,使用默认配置 + initDefaultMyChannels(); + } + } else { + // 如果没有保存的配置,使用默认配置 + initDefaultMyChannels(); + } + } + + /** + * 初始化默认的我的频道配置 + */ + private void initDefaultMyChannels() { + myChannels.clear(); + myChannels.add(new ChannelTagAdapter.ChannelTag(0, "推荐")); + myChannels.add(new ChannelTagAdapter.ChannelTag(1, "直播")); + myChannels.add(new ChannelTagAdapter.ChannelTag(2, "视频")); + myChannels.add(new ChannelTagAdapter.ChannelTag(3, "音乐")); + Log.d(TAG, "使用默认我的频道配置"); + } +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java new file mode 100644 index 00000000..2ebbad4d --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java @@ -0,0 +1,402 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; +import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.WorksResponse; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * 我的收藏页面 - 分类显示作品收藏和直播间收藏 + */ +public class MyCollectionsActivity extends AppCompatActivity { + + private TabLayout tabLayout; + private ViewPager2 viewPager; + + public static void start(Context context) { + Intent intent = new Intent(context, MyCollectionsActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_my_collections); + + setupToolbar(); + setupViewPager(); + } + + private void setupToolbar() { + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + toolbar.setNavigationOnClickListener(v -> finish()); + } + } + + private void setupViewPager() { + tabLayout = findViewById(R.id.tabLayout); + viewPager = findViewById(R.id.viewPager); + + CollectionsPagerAdapter adapter = new CollectionsPagerAdapter(this); + viewPager.setAdapter(adapter); + + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText("作品"); + break; + case 1: + tab.setText("直播间"); + break; + } + }).attach(); + } + + /** + * ViewPager适配器 + */ + private static class CollectionsPagerAdapter extends FragmentStateAdapter { + public CollectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new CollectedWorksFragment(); + } else { + return new CollectedRoomsFragment(); + } + } + + @Override + public int getItemCount() { + return 2; + } + } + + + /** + * 收藏的作品Fragment + */ + public static class CollectedWorksFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private WorksAdapter adapter; + private final List collectedWorks = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_star_24); + if (emptyText != null) emptyText.setText("还没有收藏过作品"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new WorksAdapter(work -> { + if (work != null && getActivity() != null) { + // 从我的收藏进入,显示编辑删除菜单(如果是自己的作品) + WorkDetailActivity.start(getActivity(), String.valueOf(work.getId()), true); + } + }); + recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 4) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + ApiClient.getService(getContext()) + .getMyCollectedWorks(currentPage, 20) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) collectedWorks.clear(); + collectedWorks.addAll(pageData.getList()); + adapter.submitList(new ArrayList<>(collectedWorks)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (collectedWorks.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } + + /** + * 收藏的直播间Fragment(暂时复用点赞的直播间数据,后续可扩展) + */ + public static class CollectedRoomsFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private RoomsAdapter adapter; + private final List collectedRooms = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_live_24); + if (emptyText != null) emptyText.setText("还没有收藏过直播间"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { + if (room != null && getActivity() != null) { + Intent intent = new Intent(getActivity(), RoomDetailActivity.class); + intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); + startActivity(intent); + } + }); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 2) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + // 暂时使用点赞的直播间接口,后续可以添加专门的收藏直播间接口 + ApiClient.getService(getContext()) + .getMyLikedRooms(currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) collectedRooms.clear(); + collectedRooms.addAll(convertToRooms(pageData.getList())); + adapter.submitList(new ArrayList<>(collectedRooms)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private List convertToRooms(List> roomMaps) { + List rooms = new ArrayList<>(); + for (Map map : roomMaps) { + Room room = new Room(); + if (map.containsKey("roomId")) room.setId(map.get("roomId")); + if (map.containsKey("roomTitle")) room.setTitle((String) map.get("roomTitle")); + if (map.containsKey("streamerName")) room.setStreamerName((String) map.get("streamerName")); + if (map.containsKey("streamerId")) room.setStreamerId(((Number) map.get("streamerId")).intValue()); + if (map.containsKey("coverImage")) room.setCoverImage((String) map.get("coverImage")); + if (map.containsKey("isLive")) { + Object isLive = map.get("isLive"); + if (isLive instanceof Number) room.setLive(((Number) isLive).intValue() == 1); + else if (isLive instanceof Boolean) room.setLive((Boolean) isLive); + } + rooms.add(room); + } + return rooms; + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (collectedRooms.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java new file mode 100644 index 00000000..00aa8f65 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java @@ -0,0 +1,401 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; +import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.WorksResponse; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * 我的点赞页面 - 分类显示作品点赞和直播间点赞 + */ +public class MyLikesActivity extends AppCompatActivity { + + private TabLayout tabLayout; + private ViewPager2 viewPager; + + public static void start(Context context) { + Intent intent = new Intent(context, MyLikesActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_my_likes); + + setupToolbar(); + setupViewPager(); + } + + private void setupToolbar() { + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + toolbar.setNavigationOnClickListener(v -> finish()); + } + } + + private void setupViewPager() { + tabLayout = findViewById(R.id.tabLayout); + viewPager = findViewById(R.id.viewPager); + + LikesPagerAdapter adapter = new LikesPagerAdapter(this); + viewPager.setAdapter(adapter); + + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText("作品"); + break; + case 1: + tab.setText("直播间"); + break; + } + }).attach(); + } + + /** + * ViewPager适配器 + */ + private static class LikesPagerAdapter extends FragmentStateAdapter { + public LikesPagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new LikedWorksFragment(); + } else { + return new LikedRoomsFragment(); + } + } + + @Override + public int getItemCount() { + return 2; + } + } + + + /** + * 点赞的作品Fragment + */ + public static class LikedWorksFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private WorksAdapter adapter; + private final List likedWorks = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_like_filled_24); + if (emptyText != null) emptyText.setText("还没有点赞过作品"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new WorksAdapter(work -> { + if (work != null && getActivity() != null) { + // 从我的点赞进入,显示编辑删除菜单(如果是自己的作品) + WorkDetailActivity.start(getActivity(), String.valueOf(work.getId()), true); + } + }); + recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 4) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + ApiClient.getService(getContext()) + .getMyLikedWorks(currentPage, 20) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) likedWorks.clear(); + likedWorks.addAll(pageData.getList()); + adapter.submitList(new ArrayList<>(likedWorks)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (likedWorks.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } + + /** + * 点赞的直播间Fragment + */ + public static class LikedRoomsFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private RoomsAdapter adapter; + private final List likedRooms = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_live_24); + if (emptyText != null) emptyText.setText("还没有点赞过直播间"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { + if (room != null && getActivity() != null) { + Intent intent = new Intent(getActivity(), RoomDetailActivity.class); + intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); + startActivity(intent); + } + }); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 2) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + ApiClient.getService(getContext()) + .getMyLikedRooms(currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) likedRooms.clear(); + likedRooms.addAll(convertToRooms(pageData.getList())); + adapter.submitList(new ArrayList<>(likedRooms)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private List convertToRooms(List> roomMaps) { + List rooms = new ArrayList<>(); + for (Map map : roomMaps) { + Room room = new Room(); + if (map.containsKey("roomId")) room.setId(map.get("roomId")); + if (map.containsKey("roomTitle")) room.setTitle((String) map.get("roomTitle")); + if (map.containsKey("streamerName")) room.setStreamerName((String) map.get("streamerName")); + if (map.containsKey("streamerId")) room.setStreamerId(((Number) map.get("streamerId")).intValue()); + if (map.containsKey("coverImage")) room.setCoverImage((String) map.get("coverImage")); + if (map.containsKey("isLive")) { + Object isLive = map.get("isLive"); + if (isLive instanceof Number) room.setLive(((Number) isLive).intValue() == 1); + else if (isLive instanceof Boolean) room.setLive((Boolean) isLive); + } + rooms.add(room); + } + return rooms; + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (likedRooms.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index 0c6bbb1d..2bd6a88b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -30,7 +30,9 @@ import com.example.livestreaming.ShareUtils; import com.example.livestreaming.location.TianDiTuLocationService; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.UserInfoResponse; +import com.example.livestreaming.net.WorksResponse; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -70,6 +72,7 @@ public class ProfileActivity extends AppCompatActivity { private ActivityResultLauncher editProfileLauncher; private UserWorksAdapter worksAdapter; + private WorksAdapter myWorksAdapter; public static void start(Context context) { Intent intent = new Intent(context, ProfileActivity.class); @@ -437,13 +440,19 @@ public class ProfileActivity extends AppCompatActivity { startActivity(new Intent(this, FollowingActivity.class)); }); binding.action2.setOnClickListener(v -> { - // 我的收藏(点赞的直播间) + // 我的点赞(作品+直播间) + if (!AuthHelper.requireLogin(this, "查看点赞需要登录")) { + return; + } + MyLikesActivity.start(this); + }); + binding.action3.setOnClickListener(v -> { + // 我的收藏(作品+直播间) if (!AuthHelper.requireLogin(this, "查看收藏需要登录")) { return; } - startActivity(new Intent(this, LikedRoomsActivity.class)); + MyCollectionsActivity.start(this); }); - binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class))); binding.action4.setOnClickListener(v -> { // 我的记录 - 跳转到统一记录页面 if (!AuthHelper.requireLogin(this, "查看记录需要登录")) { @@ -474,11 +483,11 @@ public class ProfileActivity extends AppCompatActivity { }); binding.addFriendBtn.setOnClickListener(v -> { - // 检查登录状态,添加好友需要登录 - if (!AuthHelper.requireLogin(this, "添加好友需要登录")) { + // 我的挚友(原添加好友功能已在挚友页面内) + if (!AuthHelper.requireLogin(this, "查看挚友需要登录")) { return; } - AddFriendActivity.start(this); + startActivity(new Intent(this, MyFriendsActivity.class)); }); // 我的钱包按钮点击事件 @@ -554,46 +563,92 @@ public class ProfileActivity extends AppCompatActivity { } private void setupWorksRecycler() { - worksAdapter = new UserWorksAdapter(); - worksAdapter.setOnWorkClickListener(workItem -> { - if (workItem != null && !TextUtils.isEmpty(workItem.getId())) { + // 设置我的作品区域 + myWorksAdapter = new WorksAdapter(work -> { + if (work != null && work.getId() != null) { // 从"我"页面进入,显示编辑删除菜单 - WorkDetailActivity.start(this, workItem.getId(), true); + WorkDetailActivity.start(this, String.valueOf(work.getId()), true); } }); - binding.worksRecycler.setLayoutManager(new GridLayoutManager(this, 3)); - binding.worksRecycler.setAdapter(worksAdapter); - loadWorks(); + binding.myWorksRecycler.setLayoutManager(new GridLayoutManager(this, 2)); + binding.myWorksRecycler.setAdapter(myWorksAdapter); + + // 发布按钮点击事件 + binding.myWorksPublishBtn.setOnClickListener(v -> { + if (!AuthHelper.requireLogin(this, "发布作品需要登录")) { + return; + } + PublishWorkActivity.start(this); + }); + + loadMyWorks(); + } + + private void loadMyWorks() { + if (!AuthHelper.isLoggedIn(this)) { + // 未登录时显示空状态 + binding.myWorksRecycler.setVisibility(View.GONE); + binding.myWorksEmptyState.setVisibility(View.VISIBLE); + binding.myWorksCount.setText("0个作品"); + return; + } + + // 获取当前用户ID + String userIdStr = com.example.livestreaming.net.AuthStore.getUserId(this); + if (userIdStr == null || userIdStr.isEmpty()) { + Log.e(TAG, "无法获取用户ID"); + showMyWorksEmpty(); + return; + } + + int userId; + try { + userId = Integer.parseInt(userIdStr); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userIdStr); + showMyWorksEmpty(); + return; + } + + ApiClient.getService(this).getUserWorks(userId, 1, 50) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null && !pageData.getList().isEmpty()) { + List works = pageData.getList(); + binding.myWorksRecycler.setVisibility(View.VISIBLE); + binding.myWorksEmptyState.setVisibility(View.GONE); + binding.myWorksCount.setText(works.size() + "个作品"); + myWorksAdapter.submitList(works); + } else { + showMyWorksEmpty(); + } + } else { + Log.e(TAG, "加载我的作品失败: " + (response.body() != null ? response.body().getMessage() : "未知错误")); + showMyWorksEmpty(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载我的作品失败: " + t.getMessage()); + showMyWorksEmpty(); + } + }); + } + + private void showMyWorksEmpty() { + binding.myWorksRecycler.setVisibility(View.GONE); + binding.myWorksEmptyState.setVisibility(View.VISIBLE); + binding.myWorksCount.setText("0个作品"); } private void loadWorks() { - // TODO: 接入后端接口 - 获取当前用户的作品列表 - // 接口路径: GET /api/users/{userId}/works - // 请求方法: GET - // 请求头: Authorization: Bearer {token} (必填,从AuthStore获取) - // 路径参数: - // - userId: String (必填) - 当前用户ID(从token中解析获取) - // 请求参数(Query): - // - page: int (可选,默认1) - 页码 - // - pageSize: int (可选,默认20) - 每页数量 - // 返回数据格式: ApiResponse> - // 实现步骤: - // 1. 从AuthStore获取token,解析userId(或从用户信息中获取) - // 2. 调用接口获取作品列表 - // 3. 更新UI显示作品列表或空状态 - // 4. 处理错误情况(网络错误、未登录等) - // 注意: 此方法在onResume时也会调用,需要避免重复请求 - - // 临时:从本地存储加载(等待后端接口) - List works = WorkManager.getAllWorks(this); - if (works != null && !works.isEmpty()) { - binding.worksRecycler.setVisibility(View.VISIBLE); - binding.worksEmptyState.setVisibility(View.GONE); - worksAdapter.submitList(works); - } else { - binding.worksRecycler.setVisibility(View.GONE); - binding.worksEmptyState.setVisibility(View.VISIBLE); - } + // 旧方法保留兼容,实际使用loadMyWorks + loadMyWorks(); } private void showTab(int index) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java index 57ebd025..753482ee 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java @@ -83,6 +83,10 @@ public class PublishWorkActivity extends AppCompatActivity { private String selectedVisibility = "所有人可见"; // 可见范围 private String selectedCommentSetting = "所有人可评论"; // 评论设置 + // 分类选择相关 + private final List allCategories = new ArrayList<>(); + private com.example.livestreaming.net.CategoryResponse selectedCategory = null; + // 天地图定位服务继续 private TianDiTuLocationService locationService; private ActivityResultLauncher requestLocationPermissionLauncher; @@ -116,6 +120,7 @@ public class PublishWorkActivity extends AppCompatActivity { return; } } + binding = ActivityPublishWorkBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); @@ -126,6 +131,7 @@ public class PublishWorkActivity extends AppCompatActivity { setupMediaAdapter(); setupLaunchers(); setupClickListeners(); + loadCategories(); // 加载分类数据 // 如果是编辑模式,预填充数据 if (isEditMode) { @@ -185,14 +191,6 @@ public class PublishWorkActivity extends AppCompatActivity { // 更新UI显示 updateMediaDisplay(); updateCoverPreview(); - - // 更新按钮文本 - binding.publishButton.setText("保存"); - - // 更新标题 - if (getSupportActionBar() != null) { - getSupportActionBar().setTitle("编辑作品"); - } } private void setupToolbar() { @@ -200,6 +198,14 @@ public class PublishWorkActivity extends AppCompatActivity { if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); + // 根据模式设置标题 + if (isEditMode) { + getSupportActionBar().setTitle("编辑作品"); + binding.publishButton.setText("保存"); + } else { + getSupportActionBar().setTitle("发布作品"); + binding.publishButton.setText("发布"); + } } binding.toolbar.setNavigationOnClickListener(v -> finish()); } @@ -428,6 +434,9 @@ public class PublishWorkActivity extends AppCompatActivity { binding.selectCoverButton.setOnClickListener(v -> showCoverPickerDialog()); binding.publishButton.setOnClickListener(v -> publishWork()); + // 分类选择点击事件 + binding.categorySpinner.setOnClickListener(v -> showCategoryPickerDialog()); + // 封面预览点击也可以选择封面 binding.coverPreview.setOnClickListener(v -> { showCoverPickerDialog(); @@ -709,11 +718,29 @@ public class PublishWorkActivity extends AppCompatActivity { } private void publishWork() { + android.util.Log.d("PublishWork", "=== 开始发布/更新作品 ==="); + android.util.Log.d("PublishWork", "编辑模式: " + isEditMode); + android.util.Log.d("PublishWork", "作品ID: " + editWorkId); + String title = binding.titleEditText.getText() != null ? binding.titleEditText.getText().toString().trim() : ""; String description = binding.descriptionEditText.getText() != null ? binding.descriptionEditText.getText().toString().trim() : ""; + android.util.Log.d("PublishWork", "标题: " + title); + android.util.Log.d("PublishWork", "描述: " + description); + android.util.Log.d("PublishWork", "作品类型: " + currentWorkType); + android.util.Log.d("PublishWork", "媒体文件数量: " + selectedMediaUris.size()); + + for (int i = 0; i < selectedMediaUris.size(); i++) { + Uri uri = selectedMediaUris.get(i); + android.util.Log.d("PublishWork", "媒体文件 " + (i+1) + ": " + uri.toString()); + android.util.Log.d("PublishWork", " -> 是否本地文件: " + isLocalUri(uri)); + } + + android.util.Log.d("PublishWork", "封面URI: " + (selectedCoverUri != null ? selectedCoverUri.toString() : "null")); + android.util.Log.d("PublishWork", "封面是否本地文件: " + (selectedCoverUri != null ? isLocalUri(selectedCoverUri) : "N/A")); + // 验证标题 if (TextUtils.isEmpty(title)) { Toast.makeText(this, "请输入作品标题", Toast.LENGTH_SHORT).show(); @@ -748,23 +775,38 @@ public class PublishWorkActivity extends AppCompatActivity { // 显示加载对话框 android.app.ProgressDialog progressDialog = new android.app.ProgressDialog(this); - progressDialog.setMessage("正在发布作品..."); + progressDialog.setMessage(isEditMode ? "正在更新作品..." : "正在发布作品..."); progressDialog.setCancelable(false); progressDialog.show(); // 开始上传流程 if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) { - // 视频作品:先上传封面,再上传视频,最后发布 - uploadCoverImage(selectedCoverUri != null ? selectedCoverUri : selectedVideoUri, - new UploadCallback() { - @Override - public void onSuccess(String url) { - String coverUrl = url; - // 上传视频 + // 视频作品处理 + handleVideoWorkUpload(title, description, progressDialog); + } else { + // 图片作品处理 + handleImageWorkUpload(title, description, progressDialog); + } + } + + /** + * 处理视频作品上传 + */ + private void handleVideoWorkUpload(String title, String description, android.app.ProgressDialog progressDialog) { + // 检查视频是否是本地文件(需要上传)还是网络URL(编辑模式,无需上传) + boolean needUploadVideo = isLocalUri(selectedVideoUri); + boolean needUploadCover = selectedCoverUri != null && isLocalUri(selectedCoverUri); + + if (needUploadCover) { + // 需要上传新封面 + uploadCoverImage(selectedCoverUri, new UploadCallback() { + @Override + public void onSuccess(String coverUrl) { + if (needUploadVideo) { + // 需要上传新视频 uploadVideo(selectedVideoUri, new UploadCallback() { @Override public void onSuccess(String videoUrl) { - // 发布作品 publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog); } @@ -774,36 +816,11 @@ public class PublishWorkActivity extends AppCompatActivity { Toast.makeText(PublishWorkActivity.this, "视频上传失败: " + error, Toast.LENGTH_SHORT).show(); } }); + } else { + // 使用原有视频URL + String videoUrl = selectedVideoUri.toString(); + 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(); - } - }); - } 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 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 @@ -812,8 +829,140 @@ public class PublishWorkActivity extends AppCompatActivity { Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show(); } }); + } else if (needUploadVideo) { + // 只需要上传视频,使用原有封面 + String coverUrl = selectedCoverUri != null ? selectedCoverUri.toString() : selectedVideoUri.toString(); + 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(); + } + }); + } else { + // 都不需要上传,直接使用原有URL + String coverUrl = selectedCoverUri != null ? selectedCoverUri.toString() : selectedVideoUri.toString(); + String videoUrl = selectedVideoUri.toString(); + publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog); } } + + /** + * 处理图片作品上传 + */ + private void handleImageWorkUpload(String title, String description, android.app.ProgressDialog progressDialog) { + android.util.Log.d("PublishWork", "=== 处理图片作品上传 ==="); + android.util.Log.d("PublishWork", "selectedMediaUris数量: " + selectedMediaUris.size()); + + // 分离本地文件和网络URL + List localImageUris = new ArrayList<>(); + List existingImageUrls = new ArrayList<>(); + + for (int i = 0; i < selectedMediaUris.size(); i++) { + Uri uri = selectedMediaUris.get(i); + android.util.Log.d("PublishWork", "图片" + i + ": " + uri.toString()); + if (isLocalUri(uri)) { + localImageUris.add(uri); + android.util.Log.d("PublishWork", " -> 本地文件,需要上传"); + } else { + existingImageUrls.add(uri.toString()); + android.util.Log.d("PublishWork", " -> 网络URL,保留原有"); + } + } + + android.util.Log.d("PublishWork", "本地图片数量: " + localImageUris.size()); + android.util.Log.d("PublishWork", "原有图片数量: " + existingImageUrls.size()); + + boolean needUploadCover = selectedCoverUri != null && isLocalUri(selectedCoverUri); + android.util.Log.d("PublishWork", "需要上传封面: " + needUploadCover); + + Uri coverUri = selectedCoverUri != null ? selectedCoverUri : + (!selectedMediaUris.isEmpty() ? selectedMediaUris.get(0) : null); + + if (needUploadCover) { + android.util.Log.d("PublishWork", "开始上传新封面..."); + // 需要上传新封面 + uploadCoverImage(selectedCoverUri, new UploadCallback() { + @Override + public void onSuccess(String coverUrl) { + android.util.Log.d("PublishWork", "封面上传成功: " + coverUrl); + if (!localImageUris.isEmpty()) { + android.util.Log.d("PublishWork", "开始上传新图片..."); + // 需要上传新图片 + uploadImages(localImageUris, new UploadImagesCallback() { + @Override + public void onSuccess(List newImageUrls) { + android.util.Log.d("PublishWork", "新图片上传成功,数量: " + newImageUrls.size()); + // 合并新上传的图片URL和原有的图片URL + List allImageUrls = new ArrayList<>(existingImageUrls); + allImageUrls.addAll(newImageUrls); + android.util.Log.d("PublishWork", "合并后图片总数: " + allImageUrls.size()); + publishWorkToServer(title, description, "IMAGE", coverUrl, null, allImageUrls, progressDialog); + } + + @Override + public void onFailure(String error) { + android.util.Log.e("PublishWork", "图片上传失败: " + error); + progressDialog.dismiss(); + Toast.makeText(PublishWorkActivity.this, "图片上传失败: " + error, Toast.LENGTH_SHORT).show(); + } + }); + } else { + android.util.Log.d("PublishWork", "无新图片需要上传,使用原有图片"); + // 只使用原有图片 + publishWorkToServer(title, description, "IMAGE", coverUrl, null, existingImageUrls, progressDialog); + } + } + + @Override + public void onFailure(String error) { + android.util.Log.e("PublishWork", "封面上传失败: " + error); + progressDialog.dismiss(); + Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show(); + } + }); + } else if (!localImageUris.isEmpty()) { + android.util.Log.d("PublishWork", "无需上传封面,开始上传新图片..."); + // 只需要上传新图片,使用原有封面 + String coverUrl = coverUri != null ? coverUri.toString() : ""; + uploadImages(localImageUris, new UploadImagesCallback() { + @Override + public void onSuccess(List newImageUrls) { + android.util.Log.d("PublishWork", "新图片上传成功,数量: " + newImageUrls.size()); + List allImageUrls = new ArrayList<>(existingImageUrls); + allImageUrls.addAll(newImageUrls); + android.util.Log.d("PublishWork", "合并后图片总数: " + allImageUrls.size()); + publishWorkToServer(title, description, "IMAGE", coverUrl, null, allImageUrls, progressDialog); + } + + @Override + public void onFailure(String error) { + android.util.Log.e("PublishWork", "图片上传失败: " + error); + progressDialog.dismiss(); + Toast.makeText(PublishWorkActivity.this, "图片上传失败: " + error, Toast.LENGTH_SHORT).show(); + } + }); + } else { + android.util.Log.d("PublishWork", "无新文件需要上传,直接使用原有URL"); + // 都不需要上传,直接使用原有URL + String coverUrl = coverUri != null ? coverUri.toString() : ""; + publishWorkToServer(title, description, "IMAGE", coverUrl, null, existingImageUrls, progressDialog); + } + } + + /** + * 判断URI是否是本地文件 + */ + private boolean isLocalUri(Uri uri) { + if (uri == null) return false; + String scheme = uri.getScheme(); + return "file".equals(scheme) || "content".equals(scheme); + } /** * 上传封面图片 @@ -1009,20 +1158,12 @@ public class PublishWorkActivity extends AppCompatActivity { } /** - * 发布作品到服务器 + * 发布或更新作品到服务器 */ private void publishWorkToServer(String title, String description, String type, String coverUrl, String videoUrl, List 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); - request.setStatus(1); // 默认状态为正常 - request.setLocation(selectedLocation); // 设置位置信息 // 如果是编辑模式,设置作品ID if (isEditMode && !TextUtils.isEmpty(editWorkId)) { @@ -1035,6 +1176,15 @@ public class PublishWorkActivity extends AppCompatActivity { } } + request.setTitle(title); + request.setDescription(description); + request.setType(type); + request.setCoverUrl(coverUrl); + request.setVideoUrl(videoUrl); + request.setImageUrls(imageUrls); + request.setStatus(1); // 默认状态为正常 + request.setLocation(selectedLocation); // 设置位置信息 + // 设置可见范围 String visibilityValue = "PUBLIC"; // 默认所有人可见 if ("所有人可见".equals(selectedVisibility)) { @@ -1056,31 +1206,36 @@ public class PublishWorkActivity extends AppCompatActivity { commentSettingValue = "DISABLED"; } request.setCommentSetting(commentSettingValue); + + // 设置分类ID + if (selectedCategory != null) { + request.setCategoryId(selectedCategory.getId()); + } ApiService apiService = ApiClient.getService(this); - // 根据模式选择不同的API调用 if (isEditMode) { - // 编辑模式:调用更新接口 - Call> call = apiService.updateWork(request); - call.enqueue(new retrofit2.Callback>() { + // 编辑模式:调用更新API + Call> updateCall = apiService.updateWork(request); + updateCall.enqueue(new retrofit2.Callback>() { @Override public void onResponse(Call> call, retrofit2.Response> response) { progressDialog.dismiss(); if (response.isSuccessful() && response.body() != null) { ApiResponse apiResponse = response.body(); - if (apiResponse.getCode() == 200 && Boolean.TRUE.equals(apiResponse.getData())) { - Toast.makeText(PublishWorkActivity.this, "保存成功", Toast.LENGTH_SHORT).show(); + if (apiResponse.getCode() == 200) { + Toast.makeText(PublishWorkActivity.this, "更新成功", Toast.LENGTH_SHORT).show(); + + // 设置结果并返回 setResult(RESULT_OK); finish(); } else { - Toast.makeText(PublishWorkActivity.this, - apiResponse.getMessage() != null ? apiResponse.getMessage() : "保存失败", - Toast.LENGTH_SHORT).show(); + String errorMessage = apiResponse.getMessage() != null ? apiResponse.getMessage() : "更新失败"; + Toast.makeText(PublishWorkActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); } } else { - Toast.makeText(PublishWorkActivity.this, "保存失败", Toast.LENGTH_SHORT).show(); + Toast.makeText(PublishWorkActivity.this, "更新失败", Toast.LENGTH_SHORT).show(); } } @@ -1091,9 +1246,9 @@ public class PublishWorkActivity extends AppCompatActivity { } }); } else { - // 发布模式:调用发布接口 - Call> call = apiService.publishWork(request); - call.enqueue(new retrofit2.Callback>() { + // 发布模式:调用发布API + Call> publishCall = apiService.publishWork(request); + publishCall.enqueue(new retrofit2.Callback>() { @Override public void onResponse(Call> call, retrofit2.Response> response) { progressDialog.dismiss(); @@ -1102,11 +1257,13 @@ public class PublishWorkActivity extends AppCompatActivity { ApiResponse apiResponse = response.body(); if (apiResponse.getCode() == 200) { Toast.makeText(PublishWorkActivity.this, "发布成功", Toast.LENGTH_SHORT).show(); + + // 设置结果并返回 + setResult(RESULT_OK); finish(); } else { - Toast.makeText(PublishWorkActivity.this, - apiResponse.getMessage() != null ? apiResponse.getMessage() : "发布失败", - Toast.LENGTH_SHORT).show(); + String errorMessage = apiResponse.getMessage() != null ? apiResponse.getMessage() : "发布失败"; + Toast.makeText(PublishWorkActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(PublishWorkActivity.this, "发布失败", Toast.LENGTH_SHORT).show(); @@ -1614,5 +1771,102 @@ public class PublishWorkActivity extends AppCompatActivity { } } } -} + + /** + * 加载分类数据 + */ + private void loadCategories() { + ApiService apiService = ApiClient.getService(this); + if (apiService == null) { + android.util.Log.e("PublishWork", "ApiService 为空,无法加载分类"); + return; + } + + Call>> call = + apiService.getLiveRoomCategories(); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + try { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + allCategories.clear(); + allCategories.addAll(apiResponse.getData()); + android.util.Log.d("PublishWork", "加载分类成功: " + allCategories.size() + " 个分类"); + } else { + android.util.Log.w("PublishWork", "分类数据为空或API返回错误"); + } + } else { + android.util.Log.e("PublishWork", "加载分类失败: " + response.code()); + } + } catch (Exception e) { + android.util.Log.e("PublishWork", "处理分类数据异常", e); + } + } + @Override + public void onFailure(Call>> call, Throwable t) { + android.util.Log.e("PublishWork", "加载分类网络错误", t); + } + }); + } + + /** + * 显示分类选择对话框(单选) + */ + private void showCategoryPickerDialog() { + if (allCategories.isEmpty()) { + Toast.makeText(this, "分类数据加载中,请稍后再试", Toast.LENGTH_SHORT).show(); + return; + } + + // 创建分类名称数组 + String[] categoryNames = new String[allCategories.size()]; + int selectedIndex = -1; + + for (int i = 0; i < allCategories.size(); i++) { + com.example.livestreaming.net.CategoryResponse category = allCategories.get(i); + categoryNames[i] = category.getName(); + // 检查是否是当前选中的分类 + if (selectedCategory != null && selectedCategory.getId().equals(category.getId())) { + selectedIndex = i; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择分类"); + + builder.setSingleChoiceItems(categoryNames, selectedIndex, (dialog, which) -> { + selectedCategory = allCategories.get(which); + updateCategoryDisplay(); + Toast.makeText(this, "已选择分类:" + selectedCategory.getName(), Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + builder.setNegativeButton("取消", null); + + builder.setNeutralButton("清空", (dialog, which) -> { + selectedCategory = null; + updateCategoryDisplay(); + Toast.makeText(this, "已清空分类选择", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + builder.show(); + } + + /** + * 更新分类显示 + */ + private void updateCategoryDisplay() { + if (selectedCategory == null) { + binding.categorySpinner.setText(""); + binding.categorySpinner.setHint("请选择分类"); + } else { + binding.categorySpinner.setText(selectedCategory.getName()); + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index 5d22ec6d..1f114cca 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -440,26 +440,16 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold // 加载点赞数 loadLikeCount(); - // 点赞按钮点击事件 + // 点赞按钮点击事件 - TRTC风格动画 likeButton.setOnClickListener(v -> { if (!AuthHelper.isLoggedIn(this)) { Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show(); return; } - // 点赞动画 - likeButton.animate() - .scaleX(1.3f) - .scaleY(1.3f) - .setDuration(100) - .withEndAction(() -> { - likeButton.animate() - .scaleX(1.0f) - .scaleY(1.0f) - .setDuration(100) - .start(); - }) - .start(); + // TRTC风格点赞动画 + android.view.animation.Animation likeAnim = android.view.animation.AnimationUtils.loadAnimation(this, R.anim.trtc_like_animation); + likeButton.startAnimation(likeAnim); // 调用点赞API likeRoom(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java index def4ea9a..0effe316 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java @@ -23,6 +23,8 @@ import com.example.livestreaming.net.HotSearchResponse; import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.WorksResponse; +import com.example.livestreaming.net.SearchUserResponse; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.tabs.TabLayout; @@ -47,10 +49,14 @@ public class SearchActivity extends AppCompatActivity { // 主播列表 private SearchStreamerAdapter streamersAdapter; private final List> streamersList = new ArrayList<>(); + + // 作品列表 + private WorksAdapter worksAdapter; + private final List worksList = new ArrayList<>(); private boolean isSearching = false; private String lastSearchKeyword = ""; - private int currentTab = 0; // 0=直播间, 1=主播 + private int currentTab = 0; // 0=直播间, 1=主播, 2=作品 private static final String EXTRA_SEARCH_QUERY = "search_query"; @@ -118,11 +124,11 @@ public class SearchActivity extends AppCompatActivity { streamersAdapter.setOnStreamerClickListener(new SearchStreamerAdapter.OnStreamerClickListener() { @Override public void onStreamerClick(Map streamer) { - // 点击主播,跳转到主播主页 + // 点击用户,跳转到用户主页 Object id = streamer.get("id"); if (id != null) { - int streamerId = ((Number) id).intValue(); - UserProfileActivity.start(SearchActivity.this, streamerId); + int userId = ((Number) id).intValue(); + UserProfileReadOnlyActivity.start(SearchActivity.this, userId); } } @@ -135,12 +141,24 @@ public class SearchActivity extends AppCompatActivity { binding.streamersRecyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.streamersRecyclerView.setAdapter(streamersAdapter); + + // 作品适配器 + worksAdapter = new WorksAdapter(work -> { + if (work == null) return; + WorkDetailActivity.start(SearchActivity.this, String.valueOf(work.getId())); + }); + + StaggeredGridLayoutManager worksLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); + worksLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); + binding.worksRecyclerView.setLayoutManager(worksLayoutManager); + binding.worksRecyclerView.setAdapter(worksAdapter); } private void setupTabs() { // 添加Tab binding.searchTabs.addTab(binding.searchTabs.newTab().setText("直播间")); - binding.searchTabs.addTab(binding.searchTabs.newTab().setText("主播")); + binding.searchTabs.addTab(binding.searchTabs.newTab().setText("用户")); + binding.searchTabs.addTab(binding.searchTabs.newTab().setText("作品")); binding.searchTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override @@ -216,6 +234,149 @@ public class SearchActivity extends AppCompatActivity { binding.hotSearchContainer.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE); + // 清空之前的结果 + roomsList.clear(); + streamersList.clear(); + worksList.clear(); + + ApiService apiService = ApiClient.getService(this); + + // 同时搜索直播间+用户和作品 + final String searchKeyword = keyword; + final int[] completedCount = {0}; + final int totalRequests = 2; + + // 搜索直播间和用户(使用原来的综合搜索API) + Call>> roomsCall = + apiService.comprehensiveSearch(searchKeyword, 1, 20); + roomsCall.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map data = apiResponse.getData(); + + // 解析直播间列表 + Object roomsObj = data.get("rooms"); + if (roomsObj instanceof List) { + List rooms = (List) roomsObj; + for (Object item : rooms) { + if (item instanceof Map) { + Room room = parseRoomFromMap((Map) item); + if (room != null) { + roomsList.add(room); + } + } + } + } + + // 解析用户列表(使用原来的streamers字段) + Object streamersObj = data.get("streamers"); + if (streamersObj instanceof List) { + List streamers = (List) streamersObj; + for (Object item : streamers) { + if (item instanceof Map) { + streamersList.add((Map) item); + } + } + } + } + } + + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "搜索直播间和用户失败", t); + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + }); + + // 搜索作品 + Map worksRequest = new HashMap<>(); + worksRequest.put("keyword", searchKeyword); + worksRequest.put("page", 1); + worksRequest.put("pageSize", 20); + + Call>> worksCall = + apiService.searchWorks(worksRequest); + worksCall.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List works = apiResponse.getData().getList(); + if (works != null) { + worksList.addAll(works); + } + } + } + + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "搜索作品失败", t); + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + }); + } + + /** + * 搜索完成后的处理 + */ + private void onSearchCompleted() { + isSearching = false; + binding.loadingProgress.setVisibility(View.GONE); + + int roomsTotal = roomsList.size(); + int usersTotal = streamersList.size(); + int worksTotal = worksList.size(); + + Log.d(TAG, "搜索完成,直播间: " + roomsTotal + ", 用户: " + usersTotal + ", 作品: " + worksTotal); + + // 更新Tab标题显示数量 + updateTabTitles(roomsTotal, usersTotal, worksTotal); + + // 显示搜索结果 + showSearchResults(); + } + + /** + * 执行综合搜索(旧方法,保留备用) + */ + private void performSearchOld(String keyword) { + if (keyword == null || keyword.trim().isEmpty()) { + return; + } + + keyword = keyword.trim(); + + Log.d(TAG, "执行综合搜索: " + keyword); + + // 显示加载状态 + binding.loadingProgress.setVisibility(View.VISIBLE); + binding.hotSearchContainer.setVisibility(View.GONE); + binding.emptyStateView.setVisibility(View.GONE); + ApiService apiService = ApiClient.getService(this); Call>> call = apiService.comprehensiveSearch(keyword, 1, 20); @@ -263,11 +424,12 @@ public class SearchActivity extends AppCompatActivity { // 获取总数 int roomsTotal = getIntValue(data.get("roomsTotal"), 0); int streamersTotal = getIntValue(data.get("streamersTotal"), 0); + int worksTotal = 0; // 旧方法没有作品数据 Log.d(TAG, "搜索成功,直播间: " + roomsTotal + ", 主播: " + streamersTotal); // 更新Tab标题显示数量 - updateTabTitles(roomsTotal, streamersTotal); + updateTabTitles(roomsTotal, streamersTotal, worksTotal); // 显示搜索结果 showSearchResults(); @@ -301,15 +463,19 @@ public class SearchActivity extends AppCompatActivity { /** * 更新Tab标题显示数量 */ - private void updateTabTitles(int roomsCount, int streamersCount) { + private void updateTabTitles(int roomsCount, int streamersCount, int worksCount) { TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); + TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2); if (roomsTab != null) { roomsTab.setText("直播间 " + roomsCount); } if (streamersTab != null) { - streamersTab.setText("主播 " + streamersCount); + streamersTab.setText("用户 " + streamersCount); + } + if (worksTab != null) { + worksTab.setText("作品 " + worksCount); } } @@ -323,6 +489,7 @@ public class SearchActivity extends AppCompatActivity { // 更新列表数据 roomsAdapter.submitList(new ArrayList<>(roomsList)); streamersAdapter.submitList(new ArrayList<>(streamersList)); + worksAdapter.submitList(new ArrayList<>(worksList)); // 根据当前Tab显示对应内容 updateTabContent(); @@ -336,6 +503,7 @@ public class SearchActivity extends AppCompatActivity { // 显示直播间 binding.roomsRecyclerView.setVisibility(View.VISIBLE); binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.GONE); if (roomsList.isEmpty()) { binding.emptyStateView.setNoSearchResultsState(); @@ -343,10 +511,11 @@ public class SearchActivity extends AppCompatActivity { } else { binding.emptyStateView.setVisibility(View.GONE); } - } else { + } else if (currentTab == 1) { // 显示主播 binding.roomsRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.VISIBLE); + binding.worksRecyclerView.setVisibility(View.GONE); if (streamersList.isEmpty()) { binding.emptyStateView.setNoSearchResultsState(); @@ -354,6 +523,18 @@ public class SearchActivity extends AppCompatActivity { } else { binding.emptyStateView.setVisibility(View.GONE); } + } else { + // 显示作品 + binding.roomsRecyclerView.setVisibility(View.GONE); + binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.VISIBLE); + + if (worksList.isEmpty()) { + binding.emptyStateView.setNoSearchResultsState(); + binding.emptyStateView.setVisibility(View.VISIBLE); + } else { + binding.emptyStateView.setVisibility(View.GONE); + } } } @@ -364,14 +545,17 @@ public class SearchActivity extends AppCompatActivity { binding.searchTabs.setVisibility(View.GONE); binding.roomsRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE); binding.hotSearchContainer.setVisibility(View.VISIBLE); // 重置Tab标题 TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); + TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2); if (roomsTab != null) roomsTab.setText("直播间"); - if (streamersTab != null) streamersTab.setText("主播"); + if (streamersTab != null) streamersTab.setText("用户"); + if (worksTab != null) worksTab.setText("作品"); } /** @@ -451,21 +635,21 @@ public class SearchActivity extends AppCompatActivity { * 处理关注点击 */ private void handleFollowClick(Map streamer, int position) { - if (!AuthHelper.requireLogin(this, "关注主播需要登录")) { + if (!AuthHelper.requireLogin(this, "关注用户需要登录")) { return; } Object id = streamer.get("id"); if (id == null) return; - int streamerId = ((Number) id).intValue(); + int userId = ((Number) id).intValue(); Object isFollowing = streamer.get("isFollowing"); boolean following = isFollowing instanceof Boolean && (Boolean) isFollowing; String action = following ? "unfollow" : "follow"; Map body = new HashMap<>(); - body.put("followedId", streamerId); + body.put("userId", userId); ApiService apiService = ApiClient.getService(this); Call>> call = following ? diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java index bcb39b2c..a0528f9a 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java @@ -68,6 +68,15 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { context.startActivity(intent); } + /** + * 只传userId启动,其他信息从后端获取 + */ + public static void start(Context context, int userId) { + Intent intent = new Intent(context, UserProfileReadOnlyActivity.class); + intent.putExtra(EXTRA_USER_ID, String.valueOf(userId)); + context.startActivity(intent); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -122,6 +131,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { // 检查关注状态 checkFollowStatus(); + // 初始化关注按钮显示(默认显示"关注") + updateFollowButton(); + // 检查好友状态 checkFriendStatus(); @@ -285,11 +297,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { if (isFollowing) { binding.followButton.setText("已关注"); binding.followButton.setSelected(true); + binding.followButton.setBackgroundResource(R.drawable.bg_follow_button_followed); binding.followButton.setTextColor(getResources().getColor(android.R.color.darker_gray, null)); } else { binding.followButton.setText("关注"); binding.followButton.setSelected(false); - binding.followButton.setTextColor(getResources().getColor(R.color.red_primary, null)); + binding.followButton.setBackgroundResource(R.drawable.bg_follow_button_normal); + binding.followButton.setTextColor(0xFF333333); } } @@ -502,7 +516,168 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { private void bindDemoStatsAndWorks(String userId) { if (binding == null) return; - String seed = !TextUtils.isEmpty(userId) ? userId : "demo"; + // 如果只传了userId,需要从后端获取用户信息 + if (!TextUtils.isEmpty(userId)) { + loadUserProfile(userId); + loadUserWorks(userId); + } else { + // 没有userId,显示演示数据 + showDemoData(); + } + } + + /** + * 从后端加载用户主页信息 + */ + private void loadUserProfile(String userId) { + try { + int uid = Integer.parseInt(userId); + ApiService apiService = ApiClient.getService(this); + Call>> call = apiService.getUserProfile(uid); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map data = apiResponse.getData(); + + // 更新用户名 + String nickname = (String) data.get("nickname"); + if (!TextUtils.isEmpty(nickname)) { + currentUserName = nickname; + binding.name.setText(nickname); + } + + // 更新头像 + String avatar = (String) data.get("avatar"); + if (!TextUtils.isEmpty(avatar)) { + com.bumptech.glide.Glide.with(UserProfileReadOnlyActivity.this) + .load(avatar) + .placeholder(R.drawable.wish_tree_checker_backup) + .error(R.drawable.wish_tree_checker_backup) + .circleCrop() + .into(binding.avatar); + } + + // 更新统计数据 + Object worksCount = data.get("worksCount"); + Object followingCount = data.get("followingCount"); + Object followersCount = data.get("followersCount"); + Object likesCount = data.get("likesCount"); + + if (worksCount != null) { + binding.statWorksValue.setText(formatIntValue(worksCount)); + } + if (followingCount != null) { + binding.statFollowingValue.setText(formatIntValue(followingCount)); + } + if (followersCount != null) { + binding.statFollowersValue.setText(formatIntValue(followersCount)); + } + if (likesCount != null) { + binding.statLikesValue.setText(formatIntValue(likesCount)); + } + + // 更新签名 + String mark = (String) data.get("mark"); + if (!TextUtils.isEmpty(mark)) { + binding.bio.setText(mark); + } + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载用户信息失败", t); + } + }); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userId); + } + } + + /** + * 从后端加载用户作品列表 + */ + private void loadUserWorks(String userId) { + try { + int uid = Integer.parseInt(userId); + ApiService apiService = ApiClient.getService(this); + Call>> call = + apiService.getUserWorks(uid, 1, 50); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List worksList = apiResponse.getData().getList(); + + if (worksList != null && !worksList.isEmpty()) { + // 转换为WorkItem列表 + List works = new ArrayList<>(); + for (com.example.livestreaming.net.WorksResponse wr : worksList) { + WorkItem work = new WorkItem(); + work.setId(String.valueOf(wr.getId())); + work.setTitle(wr.getTitle()); + work.setDescription(wr.getDescription()); + work.setCoverUrl(wr.getCoverUrl()); + work.setVideoUrl(wr.getVideoUrl()); + work.setImageUrls(wr.getImageUrls()); + work.setLikeCount(wr.getLikeCount()); + work.setCollectCount(wr.getCollectCount()); + work.setViewCount(wr.getViewCount()); + work.setType("VIDEO".equals(wr.getType()) ? WorkItem.WorkType.VIDEO : WorkItem.WorkType.IMAGE); + works.add(work); + } + + // 更新作品数量 + binding.statWorksValue.setText(String.valueOf(works.size())); + + // 设置适配器 + if (worksAdapter != null) { + worksAdapter.setOnWorkClickListener(workItem -> { + if (workItem != null && !TextUtils.isEmpty(workItem.getId())) { + WorkDetailActivity.start(UserProfileReadOnlyActivity.this, workItem.getId()); + } + }); + worksAdapter.submitList(works); + } + } else { + // 没有作品,显示空状态 + if (worksAdapter != null) { + worksAdapter.submitList(new ArrayList<>()); + } + } + } else { + Log.e(TAG, "获取作品失败: " + (apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误")); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载用户作品失败", t); + Toast.makeText(UserProfileReadOnlyActivity.this, "加载作品失败", Toast.LENGTH_SHORT).show(); + } + }); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userId); + Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 显示演示数据(当没有userId时) + */ + private void showDemoData() { + String seed = "demo"; int h = Math.abs(seed.hashCode()); int worksCount = 6 + (h % 18); @@ -517,16 +692,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { // 创建演示作品列表 List works = new ArrayList<>(); - int[] pool = new int[] { - R.drawable.wish_tree_checker_backup, - R.drawable.wish_tree_prev_no_bg, - R.drawable.wish_tree_trim_backup, - R.drawable.wish_tree_black, - R.drawable.wish_tree - }; for (int i = 0; i < 12; i++) { WorkItem work = new WorkItem(); - work.setId("demo_" + userId + "_" + i); + work.setId("demo_" + i); work.setTitle("演示作品 " + (i + 1)); work.setType(WorkItem.WorkType.IMAGE); work.setLikeCount((h + i) % 100); @@ -643,4 +811,23 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { } }); } + + /** + * 将数值转换为整数字符串显示 + */ + private String formatIntValue(Object value) { + if (value == null) { + return "0"; + } + try { + if (value instanceof Number) { + return String.valueOf(((Number) value).intValue()); + } + // 尝试解析字符串 + double d = Double.parseDouble(value.toString()); + return String.valueOf((int) d); + } catch (Exception e) { + return "0"; + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java index 04962b3d..fa120a80 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java @@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog; import java.util.ArrayList; import java.util.List; +import java.util.Map; import retrofit2.Call; @@ -44,6 +45,7 @@ public class WorkDetailActivity extends AppCompatActivity { // 状态标记 private boolean isLiked = false; private boolean isFavorited = false; + private boolean isFollowed = false; // 是否已关注作者 private int commentCount = 0; private static final String EXTRA_WORK_ID = "work_id"; @@ -804,40 +806,213 @@ public class WorkDetailActivity extends AppCompatActivity { } } - private void setupActionButton() { - // ============================================ - // TODO: 判断是否是当前用户的作品 - // ============================================ - // 判断方式(推荐方式3): - // 1. 从WorkItem中获取userId字段,与当前登录用户ID比较(需要从AuthStore获取当前用户ID) - // 2. 或调用接口: GET /api/works/{workId}/isOwner - // 3. 或从作品详情接口返回的WorkItem中包含isOwner字段(推荐,已在获取详情时返回) - // - // 如果使用方式3(推荐): - // - 在获取作品详情时,后端已经返回isOwner字段 - // - 直接使用 workItem.getIsOwner() 或从详情接口返回的isOwner字段 - // - 如果isOwner为true,显示编辑/删除按钮 - // - 如果isOwner为false或用户未登录,隐藏编辑/删除按钮 - // - // 如果使用方式1: - // - 需要从AuthStore获取当前登录用户的userId - // - 比较 workItem.getUserId() 与当前用户ID - // - 如果相同,显示编辑/删除按钮;否则隐藏 - // - // 如果使用方式2: - // 接口路径: GET /api/works/{workId}/isOwner - // 请求方法: GET - // 请求头: - // - Authorization: Bearer {token} (必填,需要登录) - // 路径参数: - // - workId: String (必填) - 作品ID - // 返回数据格式: ApiResponse - // 后端返回: { "code": 200, "data": { "isOwner": true/false } } - // - // 目前简化处理,所有作品都显示操作按钮 - // TODO: 根据isOwner字段控制按钮显示/隐藏 + /** + * 设置作者头像和关注按钮 + */ + private void setupAuthorSection() { + if (workItem == null) return; - // 新布局中actionButton是ImageView,不是FloatingActionButton(添加防抖) + int authorId = workItem.getAuthorId(); + String authorAvatar = workItem.getAuthorAvatar(); + + // 加载作者头像 + if (!TextUtils.isEmpty(authorAvatar)) { + Glide.with(this) + .load(authorAvatar) + .circleCrop() + .placeholder(R.drawable.ic_account_circle_24) + .error(R.drawable.ic_account_circle_24) + .into(binding.authorAvatar); + } + + // 头像点击跳转到作者主页 + binding.authorAvatar.setOnClickListener(new DebounceClickListener() { + @Override + public void onDebouncedClick(View v) { + if (authorId > 0) { + // 跳转到用户主页 + UserProfileReadOnlyActivity.start(WorkDetailActivity.this, authorId); + } + } + }); + + // 检查是否是自己的作品 + String currentUserIdStr = AuthStore.getUserId(this); + int currentUserId = 0; + if (currentUserIdStr != null) { + try { + currentUserId = Integer.parseInt(currentUserIdStr); + } catch (NumberFormatException e) { + // 忽略解析错误 + } + } + if (currentUserId > 0 && currentUserId == authorId) { + // 自己的作品,隐藏关注按钮 + binding.followButton.setVisibility(View.GONE); + } else { + // 检查关注状态 + checkFollowStatus(authorId); + + // 关注按钮点击事件 + binding.followButton.setOnClickListener(new DebounceClickListener() { + @Override + public void onDebouncedClick(View v) { + toggleFollow(authorId); + } + }); + } + } + + /** + * 检查关注状态 + */ + private void checkFollowStatus(int userId) { + if (userId <= 0) return; + + // 未登录时不检查 + if (AuthStore.getToken(this) == null) { + binding.followButton.setVisibility(View.VISIBLE); + return; + } + + ApiService apiService = ApiClient.getService(this); + Call>> call = apiService.checkFollowStatus(userId); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Object isFollowingObj = apiResponse.getData().get("isFollowing"); + isFollowed = isFollowingObj != null && Boolean.parseBoolean(isFollowingObj.toString()); + updateFollowButton(); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + android.util.Log.e("WorkDetail", "检查关注状态失败", t); + } + }); + } + + /** + * 更新关注按钮显示 + */ + private void updateFollowButton() { + if (isFollowed) { + // 已关注,隐藏+号按钮 + binding.followButton.setVisibility(View.GONE); + } else { + // 未关注,显示+号按钮 + binding.followButton.setVisibility(View.VISIBLE); + } + } + + /** + * 切换关注状态 + */ + private void toggleFollow(int userId) { + // 检查登录状态 + if (!AuthHelper.requireLogin(this, "关注需要登录")) { + return; + } + + if (userId <= 0) { + Toast.makeText(this, "用户信息无效", Toast.LENGTH_SHORT).show(); + return; + } + + ApiService apiService = ApiClient.getService(this); + + // 构建请求参数 + java.util.Map body = new java.util.HashMap<>(); + body.put("userId", userId); + + Call>> call; + if (isFollowed) { + // 取消关注 + call = apiService.unfollowUser(body); + } else { + // 关注 + call = apiService.followUser(body); + } + + // 乐观更新UI + boolean oldFollowed = isFollowed; + isFollowed = !isFollowed; + updateFollowButton(); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200) { + Toast.makeText(WorkDetailActivity.this, + isFollowed ? "关注成功" : "已取消关注", + Toast.LENGTH_SHORT).show(); + } else { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, + apiResponse.getMessage() != null ? apiResponse.getMessage() : "操作失败", + Toast.LENGTH_SHORT).show(); + } + } else { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, "操作失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + private void setupActionButton() { + // 检查是否应该显示菜单 + boolean showMenu = getIntent().getBooleanExtra(EXTRA_SHOW_MENU, false); + if (!showMenu) { + binding.actionButton.setVisibility(View.GONE); + return; + } + + // 检查是否是当前用户的作品 + String currentUserIdStr = AuthStore.getUserId(this); + if (currentUserIdStr == null || workItem == null) { + binding.actionButton.setVisibility(View.GONE); + return; + } + + try { + int currentUserId = Integer.parseInt(currentUserIdStr); + int authorId = workItem.getAuthorId(); + + if (currentUserId != authorId) { + // 不是自己的作品,隐藏菜单 + binding.actionButton.setVisibility(View.GONE); + return; + } + } catch (NumberFormatException e) { + binding.actionButton.setVisibility(View.GONE); + return; + } + + // 是自己的作品且需要显示菜单,设置点击事件 + binding.actionButton.setVisibility(View.VISIBLE); binding.actionButton.setOnClickListener(new DebounceClickListener() { @Override public void onDebouncedClick(View v) { @@ -847,10 +1022,21 @@ public class WorkDetailActivity extends AppCompatActivity { } private void showActionMenu() { - // TODO: 判断是否是当前用户的作品 - boolean isOwner = true; // 简化处理 - - if (!isOwner) { + // 检查是否是当前用户的作品 + String currentUserIdStr = AuthStore.getUserId(this); + if (currentUserIdStr == null || workItem == null) { + return; + } + + try { + int currentUserId = Integer.parseInt(currentUserIdStr); + int authorId = workItem.getAuthorId(); + + if (currentUserId != authorId) { + Toast.makeText(this, "只能编辑自己的作品", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { return; } @@ -869,58 +1055,32 @@ public class WorkDetailActivity extends AppCompatActivity { } private void editWork() { - // ============================================ - // TODO: 实现编辑作品功能 - // ============================================ - // 方案1: 跳转到编辑页面(PublishWorkActivity,传入workItem数据) - // 方案2: 在当前页面弹出编辑对话框 - // - // 如果使用方案1(推荐): - // 1. 创建EditWorkActivity或复用PublishWorkActivity - // 2. 传入workItem数据(通过Intent传递) - // 3. 在编辑页面预填充作品信息(标题、描述、图片/视频) - // 4. 用户修改后,调用更新接口保存 - // - // 更新作品接口: - // 接口路径: PUT /api/works/{workId} - // 请求方法: PUT - // 请求头: - // - Authorization: Bearer {token} (必填,需要登录) - // 路径参数: - // - workId: String (必填) - 作品ID - // 请求体 (multipart/form-data 或 JSON): - // - title: String (可选) - 作品标题 - // - description: String (可选) - 作品描述 - // - cover: File (可选) - 新的封面图片(如果要更换封面) - // - video: File (可选) - 新的视频文件(如果要更换视频,仅视频作品) - // - images: File[] (可选) - 新的图片文件数组(如果要更换图片,仅图片作品) - // - deletedImageIds: String[] (可选) - 要删除的图片ID列表(图片作品) - // 返回数据格式: ApiResponse - // - // 前端需要传入的参数: - // - workId: String (从workItem.getId()获取) - // - title: String (用户修改后的标题) - // - description: String (用户修改后的描述,可选) - // - cover: File (如果要更换封面) - // - video/images: File/File[] (如果要更换媒体文件) - // - token: String (必填,从AuthStore获取) - // - // 实现步骤: - // 1. 跳转到编辑页面,传入workItem - // 2. 在编辑页面加载作品数据并预填充表单 - // 3. 用户修改内容后点击保存 - // 4. 验证输入(标题不能为空等) - // 5. 如果有新上传的文件,先上传文件获取URL - // 6. 调用 PUT /api/works/{workId} 更新作品信息 - // 7. 更新成功后,刷新当前页面或返回并刷新列表 - // 8. 处理错误情况(未登录、无权限、文件上传失败等) - // - // 注意: - // - 只有作品作者才能编辑 - // - 如果更换媒体文件,需要先上传新文件 - // - 图片作品可以删除部分图片,需要传递deletedImageIds + // 检查登录状态 + if (!AuthHelper.requireLogin(this, "编辑作品需要登录")) { + return; + } + + if (workItem == null) { + Toast.makeText(this, "作品信息不完整", Toast.LENGTH_SHORT).show(); + return; + } + + // 跳转到编辑页面(复用PublishWorkActivity) + Intent intent = new Intent(this, PublishWorkActivity.class); + intent.putExtra("edit_mode", true); + intent.putExtra("work_id", workItem.getId()); + intent.putExtra("work_title", workItem.getTitle()); + intent.putExtra("work_description", workItem.getDescription()); + intent.putExtra("work_type", workItem.getType().name()); + intent.putExtra("work_cover_url", workItem.getCoverUrl()); - Toast.makeText(this, "编辑功能待实现", Toast.LENGTH_SHORT).show(); + if (workItem.getType() == WorkItem.WorkType.VIDEO) { + intent.putExtra("work_video_url", workItem.getVideoUrl()); + } else if (workItem.getImageUrls() != null) { + intent.putStringArrayListExtra("work_image_urls", new ArrayList<>(workItem.getImageUrls())); + } + + startActivityForResult(intent, 1001); } private void deleteWork() { @@ -1066,6 +1226,7 @@ public class WorkDetailActivity extends AppCompatActivity { setupContent(); setupActionButtons(); setupActionButton(); + setupAuthorSection(); // 设置作者头像和关注按钮 // 获取真实的评论数量 loadRealCommentCount(worksResponse.getId()); @@ -1114,6 +1275,11 @@ public class WorkDetailActivity extends AppCompatActivity { // 防止 publishTime 为 null 导致 NullPointerException item.setPublishTime(response.getPublishTime() != null ? response.getPublishTime() : 0L); + // 设置作者信息 + item.setAuthorId(response.getUserId() != null ? response.getUserId() : 0); + item.setAuthorName(response.getAuthorName() != null ? response.getAuthorName() : response.getUserName()); + item.setAuthorAvatar(response.getAuthorAvatar() != null ? response.getAuthorAvatar() : response.getUserAvatar()); + // 设置作品类型 if ("VIDEO".equals(response.getType())) { item.setType(WorkItem.WorkType.VIDEO); @@ -1156,4 +1322,5 @@ public class WorkDetailActivity extends AppCompatActivity { } } } -} \ No newline at end of file +} + diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java b/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java index 344134fa..d5f9baed 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java @@ -23,6 +23,11 @@ public class WorkItem implements Parcelable { private long publishTime; private WorkType type; // 作品类型:图片或视频 + // 作者信息 + private int authorId; // 作者用户ID + private String authorName; // 作者昵称 + private String authorAvatar; // 作者头像URL + // 本地使用的URI(发布时使用) private transient Uri coverUri; private transient Uri videoUri; @@ -41,6 +46,9 @@ public class WorkItem implements Parcelable { this.publishTime = System.currentTimeMillis(); this.imageUrls = new ArrayList<>(); this.imageUris = new ArrayList<>(); + this.authorId = 0; + this.authorName = ""; + this.authorAvatar = ""; } public WorkItem(String id, String title, String description, String coverUrl, @@ -171,6 +179,30 @@ public class WorkItem implements Parcelable { this.imageUris = imageUris; } + public int getAuthorId() { + return authorId; + } + + public void setAuthorId(int authorId) { + this.authorId = authorId; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorAvatar() { + return authorAvatar; + } + + public void setAuthorAvatar(String authorAvatar) { + this.authorAvatar = authorAvatar; + } + // Parcelable implementation protected WorkItem(Parcel in) { id = in.readString(); @@ -186,6 +218,9 @@ public class WorkItem implements Parcelable { int typeOrdinal = in.readInt(); type = typeOrdinal >= 0 && typeOrdinal < WorkType.values().length ? WorkType.values()[typeOrdinal] : WorkType.IMAGE; + authorId = in.readInt(); + authorName = in.readString(); + authorAvatar = in.readString(); } @Override @@ -201,6 +236,9 @@ public class WorkItem implements Parcelable { dest.writeInt(viewCount); dest.writeLong(publishTime); dest.writeInt(type != null ? type.ordinal() : -1); + dest.writeInt(authorId); + dest.writeString(authorName); + dest.writeString(authorAvatar); } @Override diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java index d17c01aa..f36d153f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java @@ -380,7 +380,24 @@ public interface ApiService { @GET("api/front/works/detail/{id}") Call> getWorkDetail(@Path("id") long id); - @DELETE("api/front/works/delete/{id}") + /** + * 获取当前用户发布的作品列表 + */ + @GET("api/front/works/my") + Call>> getMyPublishedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取指定用户发布的作品列表 + */ + @GET("api/front/works/user/{userId}") + Call>> getUserWorks( + @Path("userId") int userId, + @Query("page") int page, + @Query("pageSize") int pageSize); + + @POST("api/front/works/delete/{id}") Call> deleteWork(@Path("id") long id); @POST("api/front/works/like/{id}") @@ -983,31 +1000,27 @@ public interface ApiService { @Query("page") int page, @Query("limit") int limit); - // ==================== 聊天室接口 ==================== + /** + * 搜索作品(使用POST方法) + */ + @POST("api/front/works/search") + Call>> searchWorks(@Body Map request); + + // ==================== 我的点赞/收藏作品接口 ==================== /** - * 获取聊天室列表 + * 获取我点赞的作品列表 */ - @GET("api/front/chatroom/list") - Call>> getChatRoomList( + @GET("api/front/works/my/liked") + Call>> getMyLikedWorks( @Query("page") int page, - @Query("limit") int limit); + @Query("pageSize") int pageSize); /** - * 获取聊天室详情 + * 获取我收藏的作品列表 */ - @GET("api/front/chatroom/detail/{id}") - Call> getChatRoomDetail(@Path("id") int id); - - /** - * 检查是否可以进入聊天室 - */ - @GET("api/front/chatroom/check-entry/{roomId}") - Call>> checkChatRoomEntry(@Path("roomId") int roomId); - - /** - * 支付进入聊天室 - */ - @POST("api/front/chatroom/pay-entry/{roomId}") - Call>> payChatRoomEntry(@Path("roomId") int roomId); + @GET("api/front/works/my/collected") + Call>> getMyCollectedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); } diff --git a/android-app/app/src/main/res/anim/trtc_button_click.xml b/android-app/app/src/main/res/anim/trtc_button_click.xml new file mode 100644 index 00000000..0afc63cc --- /dev/null +++ b/android-app/app/src/main/res/anim/trtc_button_click.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/anim/trtc_like_animation.xml b/android-app/app/src/main/res/anim/trtc_like_animation.xml new file mode 100644 index 00000000..7fc20e33 --- /dev/null +++ b/android-app/app/src/main/res/anim/trtc_like_animation.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag.xml b/android-app/app/src/main/res/drawable/bg_channel_tag.xml index cb5ed321..c303e63d 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag.xml @@ -1,9 +1,9 @@ + + - - diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml index a0e6bba4..c303e63d 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml @@ -1,9 +1,9 @@ - - + + + android:color="#E0E0E0" /> diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml new file mode 100644 index 00000000..0ee07b0f --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml index 2d0c3668..4f560150 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml @@ -1,9 +1,9 @@ + + - - + android:width="2dp" + android:color="#1976D2" /> diff --git a/android-app/app/src/main/res/drawable/bg_circle_white.xml b/android-app/app/src/main/res/drawable/bg_circle_white.xml new file mode 100644 index 00000000..15067aa6 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_circle_white.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_follow_button.xml b/android-app/app/src/main/res/drawable/bg_follow_button.xml index 4fc40821..ed56210b 100644 --- a/android-app/app/src/main/res/drawable/bg_follow_button.xml +++ b/android-app/app/src/main/res/drawable/bg_follow_button.xml @@ -1,19 +1,8 @@ - - - - - - - - - - - - - - - - - - + + + + diff --git a/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml b/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml new file mode 100644 index 00000000..06bd22e3 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml b/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml new file mode 100644 index 00000000..0abc92db --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_trtc_button_primary.xml b/android-app/app/src/main/res/drawable/bg_trtc_button_primary.xml new file mode 100644 index 00000000..d8c4d4ac --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_trtc_button_primary.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_trtc_button_secondary.xml b/android-app/app/src/main/res/drawable/bg_trtc_button_secondary.xml new file mode 100644 index 00000000..dbe4938c --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_trtc_button_secondary.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_trtc_info_card.xml b/android-app/app/src/main/res/drawable/bg_trtc_info_card.xml new file mode 100644 index 00000000..f1aa82b2 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_trtc_info_card.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_trtc_input.xml b/android-app/app/src/main/res/drawable/bg_trtc_input.xml new file mode 100644 index 00000000..95e4759a --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_trtc_input.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/activity_my_collections.xml b/android-app/app/src/main/res/layout/activity_my_collections.xml new file mode 100644 index 00000000..93dec2b2 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_my_collections.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_my_likes.xml b/android-app/app/src/main/res/layout/activity_my_likes.xml new file mode 100644 index 00000000..66b50d07 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_my_likes.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_profile.xml b/android-app/app/src/main/res/layout/activity_profile.xml index df1436c8..d5d6af97 100644 --- a/android-app/app/src/main/res/layout/activity_profile.xml +++ b/android-app/app/src/main/res/layout/activity_profile.xml @@ -70,6 +70,7 @@ android:layout_marginTop="10dp" android:gravity="center_vertical" android:orientation="horizontal" + android:visibility="gone" app:layout_constraintHorizontal_bias="0" app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintStart_toStartOf="@id/name" @@ -187,6 +188,7 @@ android:background="@drawable/bg_circle_white_60" android:padding="8dp" android:src="@drawable/ic_clock_24" + android:visibility="gone" app:layout_constraintEnd_toStartOf="@id/topActionMore" app:layout_constraintTop_toTopOf="@id/topActionMore" /> @@ -198,6 +200,7 @@ android:background="@drawable/bg_circle_white_60" android:padding="8dp" android:src="@drawable/ic_crosshair_24" + android:visibility="gone" app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintTop_toTopOf="@id/topActionMore" /> @@ -439,7 +442,7 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" - android:src="@drawable/ic_like_24" + android:src="@drawable/ic_like_filled_24" android:tint="#FF4081" /> @@ -453,7 +456,7 @@ @@ -488,8 +491,8 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" - android:src="@drawable/ic_heart_24" - android:tint="#E91E63" /> + android:src="@drawable/ic_star_24" + android:tint="#FFA726" /> @@ -502,7 +505,7 @@ @@ -512,7 +515,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" - android:text="0人" + android:text="0个" android:textColor="#999999" android:textSize="11sp" /> @@ -525,7 +528,8 @@ android:layout_width="wrap_content" android:layout_height="64dp" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:visibility="gone"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/myWorksSection"> + + + + + + + + + + + + + + + + + + - + + android:paddingHorizontal="10dp" + android:paddingVertical="8dp"> + app:civ_border_color="@color/trtc_accent" + app:civ_border_width="2dp" /> + android:textColor="@color/trtc_text_secondary" + android:textSize="12sp" /> + app:backgroundTint="@color/trtc_accent" + app:cornerRadius="16dp" /> - + - + + android:paddingHorizontal="10dp" + android:paddingVertical="6dp"> + app:tint="@color/trtc_text_primary" /> + android:textColor="@color/trtc_text_primary" + android:textSize="13sp" + android:textStyle="bold" /> @@ -296,7 +298,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - + - + + android:layout_marginBottom="16dp"> + android:tint="@color/trtc_accent" /> + android:textColor="@color/trtc_text_primary" + android:textSize="10sp" + android:textStyle="bold" /> - + + android:tint="@color/trtc_secondary" /> - + + android:tint="@color/trtc_text_primary" /> - + + android:paddingHorizontal="16dp" + android:textColor="@color/trtc_text_primary" + android:textSize="14sp" /> + android:textSize="13sp" + android:textStyle="bold" + app:backgroundTint="@color/trtc_accent" + app:cornerRadius="21dp" /> diff --git a/android-app/app/src/main/res/layout/activity_search.xml b/android-app/app/src/main/res/layout/activity_search.xml index 4ec829d7..d3b0c50d 100644 --- a/android-app/app/src/main/res/layout/activity_search.xml +++ b/android-app/app/src/main/res/layout/activity_search.xml @@ -114,6 +114,18 @@ android:paddingBottom="24dp" android:visibility="gone" /> + + + @@ -271,7 +271,7 @@ android:id="@+id/space_addFriend_margin_top" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintHeight_percent="0.028" + app:layout_constraintHeight_percent="0.015" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/bio" /> @@ -292,10 +292,10 @@ android:layout_height="44dp" android:layout_weight="1" android:layout_marginEnd="6dp" - android:background="@drawable/bg_follow_button" + android:background="@drawable/bg_follow_button_normal" android:gravity="center" android:text="关注" - android:textColor="#FF4757" + android:textColor="#333333" android:textSize="15sp" android:textStyle="bold" /> diff --git a/android-app/app/src/main/res/layout/activity_work_detail.xml b/android-app/app/src/main/res/layout/activity_work_detail.xml index a4f82a51..a702e4cb 100644 --- a/android-app/app/src/main/res/layout/activity_work_detail.xml +++ b/android-app/app/src/main/res/layout/activity_work_detail.xml @@ -80,7 +80,7 @@ - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android-app/app/src/main/res/layout/dialog_category_management.xml b/android-app/app/src/main/res/layout/dialog_category_management.xml new file mode 100644 index 00000000..ba9be380 --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_category_management.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/fragment_list_content.xml b/android-app/app/src/main/res/layout/fragment_list_content.xml new file mode 100644 index 00000000..29491a25 --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_list_content.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_channel_tag.xml b/android-app/app/src/main/res/layout/item_channel_tag.xml index b57f7da3..2619c6d6 100644 --- a/android-app/app/src/main/res/layout/item_channel_tag.xml +++ b/android-app/app/src/main/res/layout/item_channel_tag.xml @@ -3,52 +3,31 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="4dp"> + android:layout_margin="6dp"> + android:textSize="13sp" + android:textColor="#666666" + android:background="@drawable/bg_channel_tag_normal" + android:minWidth="60dp" + android:gravity="center" /> - + - - - - - - diff --git a/android-app/app/src/main/res/values/colors_trtc.xml b/android-app/app/src/main/res/values/colors_trtc.xml new file mode 100644 index 00000000..962589aa --- /dev/null +++ b/android-app/app/src/main/res/values/colors_trtc.xml @@ -0,0 +1,29 @@ + + + + #FF6B35 + #E55A2B + #4ECDC4 + #FF4081 + #FF6FA3 + + + #1A1A1A + #2D2D2D + #3D3D3D + + + #FFFFFF + #CCFFFFFF + #80FFFFFF + + + #80000000 + #40FFFFFF + #40FF4081 + + + #FF4444 + #4CAF50 + #999999 + \ No newline at end of file diff --git a/android-app/搜索功能更新说明.md b/android-app/搜索功能更新说明.md new file mode 100644 index 00000000..af76f9f6 --- /dev/null +++ b/android-app/搜索功能更新说明.md @@ -0,0 +1,63 @@ +# 搜索功能和分类管理修复说明 + +## 一、搜索功能修复 + +### 1. 用户搜索问题修复 ✅ +**问题**:用户搜索结果始终为0 +**修复**:恢复使用 `comprehensiveSearch` API,从 `streamers` 字段获取用户数据 + +### 2. 作品搜索问题修复 ✅ +**问题**:搜索结果不准确 +**修复**:使用正确的 `POST /api/front/works/search` 接口 + +## 二、分类管理功能 + +### 1. 后端修改 +**文件**:`CategoryController.java` +- 修改 `getWorkCategories()` 方法,让作品分类使用直播间分类 +- 实现统一的分类系统(直播和作品使用相同分类) + +### 2. Android端修改 +**文件**:`MainActivity.java` +- 添加了下拉按钮(btnExpandCategories)点击事件 +- 添加了 `showCategoryManagementDialog()` 方法 +- 添加了 `setupCategoryManagementDialog()` 方法 +- 添加了 `loadCategoriesForDialog()` 方法 +- 添加了 `updateCategoryTabsFromMyChannels()` 方法 + +**文件**:`dialog_category_management.xml` +- 创建了分类管理对话框布局 +- 包含"我的频道"和"推荐频道"两个区域 +- 支持添加/移除频道 + +## 三、数据库说明 + +### 现有数据 +- **eb_live_room_category**:有5个分类(娱乐、游戏、音乐、户外、聊天) +- **eb_category**:type=8或9的数据为空 +- **eb_works**:作品的category_id都为空 + +### 统一分类方案 +- 直播和作品都使用 `eb_live_room_category` 表的分类 +- 后端 `getWorkCategories()` 接口返回直播间分类 + +## 四、测试建议 + +1. **搜索功能测试**: + - 测试用户搜索 + - 测试作品搜索(按标题模糊匹配) + - 测试直播间搜索 + +2. **分类管理测试**: + - 点击首页右上角下拉按钮 + - 验证分类管理对话框显示 + - 测试添加/移除频道功能 + +## 五、完成状态 + +✅ 搜索功能修复完成 +✅ 后端分类统一完成 +✅ 下拉按钮点击事件添加完成 +✅ 分类管理对话框创建完成 + +**可以重新编译测试了!**