Merge branch 'master' of http://115.190.64.57:8000/xiaozhang/zhibo into IM-gift

# Conflicts:
#	android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java
#	android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java
#	android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java
This commit is contained in:
ShiQi 2026-01-08 16:10:06 +08:00
commit 5191e3d484
67 changed files with 3687 additions and 1006 deletions

View File

@ -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

View File

@ -0,0 +1,46 @@
我们正在开发直播App的三个新功能请继续之前的工作
【功能1】阅后即焚图片消息
- 好友私聊中发送阅后即焚图片
- 查看几秒后自动销毁
- 需要在Android端和后端实现
【功能2】作品上热门
- 用户付费让作品上热门
- 热门作品在首页优先展示
- 需要付费档位和热门队列
【功能3】发布动态 + 附近动态
- 用户发布动态(文字+图片+位置)
- 查看附近的人的动态
- 基于地理位置发现
当前项目结构:
- Android端android-app/app/src/main/java/com/example/livestreaming/
- 后端JavaZhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/
- 后端ControllerZhibo/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/
- 后端JavaZhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/
- 后端ControllerZhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/
请根据我指定的功能继续开发。

View File

@ -1,3 +1,8 @@
# 手动引用
1. 你明白我的意思吗。有没有不清楚的问题,请先询问我然后进行开发。有歧义要先询问我之后再进行下一步,不能你自己猜测可能的结果
2.
# AI工作指南 # AI工作指南
## 🚀 快速引用 ## 🚀 快速引用

View File

@ -36,7 +36,7 @@ LIVE_PUBLIC_SRS_HTTP_PORT: 25003
file: file:
upload: upload:
server: server:
url: http://1.15.149.240:30005/upload # 文件上传服务器地址 url: http://1.15.149.240:30005/api/upload # 文件上传服务器地址
# 配置端口 # 配置端口
server: server:

View File

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

View File

@ -28,6 +28,9 @@ public class SendMessageRequest implements Serializable {
@ApiModelProperty(value = "语音时长(可选)") @ApiModelProperty(value = "语音时长(可选)")
private Integer duration; private Integer duration;
@ApiModelProperty(value = "阅后即焚秒数可选仅messageType=burn_image时使用")
private Integer burnSeconds;
// 兼容旧字段 // 兼容旧字段
public String getMessage() { public String getMessage() {
return content; return content;

View File

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

View File

@ -57,6 +57,18 @@ public class ChatMessageResponse implements Serializable {
@ApiModelProperty(value = "语音时长(秒)") @ApiModelProperty(value = "语音时长(秒)")
private Integer duration; 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 = "是否已读") @ApiModelProperty(value = "是否已读")
private Boolean isRead; private Boolean isRead;

View File

@ -67,14 +67,11 @@ public class CategoryController {
@ApiOperation(value = "获取作品分类列表") @ApiOperation(value = "获取作品分类列表")
@GetMapping("/work") @GetMapping("/work")
public CommonResult<List<CategoryResponse>> getWorkCategories() { public CommonResult<List<CategoryResponse>> getWorkCategories() {
List<Category> categories = categoryService.getList( // 暂时使用直播间分类作为作品分类实现统一分类系统
new com.zbkj.common.request.CategorySearchRequest() List<LiveRoomCategory> liveCategories = liveRoomCategoryService.getEnabledList();
.setType(CategoryConstants.CATEGORY_TYPE_WORK)
.setStatus(CategoryConstants.CATEGORY_STATUS_NORMAL)
);
List<CategoryResponse> response = categories.stream() List<CategoryResponse> response = liveCategories.stream()
.map(this::toCategoryResponse) .map(this::toLiveRoomCategoryResponse)
.collect(Collectors.toList()); .collect(Collectors.toList());
return CommonResult.success(response); return CommonResult.success(response);

View File

@ -2,6 +2,7 @@ package com.zbkj.front.controller;
import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.model.chat.Conversation;
import com.zbkj.common.request.SendMessageRequest; import com.zbkj.common.request.SendMessageRequest;
import com.zbkj.common.response.BurnViewResponse;
import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ChatMessageResponse;
import com.zbkj.common.response.ConversationResponse; import com.zbkj.common.response.ConversationResponse;
import com.zbkj.common.result.CommonResult; import com.zbkj.common.result.CommonResult;
@ -157,4 +158,12 @@ public class ConversationController {
Integer userId = userService.getUserIdException(); Integer userId = userService.getUserIdException();
return CommonResult.success(conversationService.recallMessage(id, userId)); return CommonResult.success(conversationService.recallMessage(id, userId));
} }
@ApiOperation(value = "查看阅后即焚图片(触发计时)")
@ApiImplicitParam(name = "id", value = "消息ID", required = true)
@PostMapping("/messages/{id}/burn/view")
public CommonResult<BurnViewResponse> viewBurnImage(@PathVariable Long id) {
Integer userId = userService.getUserIdException();
return CommonResult.success(conversationService.viewBurnImage(id, userId));
}
} }

View File

@ -36,6 +36,12 @@ LIVE_PUBLIC_SRS_HOST: 1.15.149.240
LIVE_PUBLIC_SRS_RTMP_PORT: 25002 LIVE_PUBLIC_SRS_RTMP_PORT: 25002
LIVE_PUBLIC_SRS_HTTP_PORT: 25003 LIVE_PUBLIC_SRS_HTTP_PORT: 25003
# ============ 文件上传服务器配置 ============
file:
upload:
server:
url: http://1.15.149.240:30005/api/upload
spring: spring:
profiles: profiles:
# 配置的环境 # 配置的环境

View File

@ -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<PrivateMessageBurn> {
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.model.chat.Conversation;
import com.zbkj.common.model.chat.PrivateMessage; import com.zbkj.common.model.chat.PrivateMessage;
import com.zbkj.common.request.SendMessageRequest; import com.zbkj.common.request.SendMessageRequest;
import com.zbkj.common.response.BurnViewResponse;
import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ChatMessageResponse;
import com.zbkj.common.response.ConversationResponse; import com.zbkj.common.response.ConversationResponse;
@ -78,4 +79,6 @@ public interface ConversationService extends IService<Conversation> {
* 发送消息带详细参数 * 发送消息带详细参数
*/ */
PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl); PrivateMessage sendMessage(Long conversationId, Integer senderId, String messageType, String content, String mediaUrl);
BurnViewResponse viewBurnImage(Long messageId, Integer userId);
} }

View File

@ -6,12 +6,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zbkj.common.exception.CrmebException; import com.zbkj.common.exception.CrmebException;
import com.zbkj.common.model.chat.Conversation; import com.zbkj.common.model.chat.Conversation;
import com.zbkj.common.model.chat.PrivateMessage; 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.model.user.User;
import com.zbkj.common.request.SendMessageRequest; import com.zbkj.common.request.SendMessageRequest;
import com.zbkj.common.response.BurnViewResponse;
import com.zbkj.common.response.ChatMessageResponse; import com.zbkj.common.response.ChatMessageResponse;
import com.zbkj.common.response.ConversationResponse; import com.zbkj.common.response.ConversationResponse;
import com.zbkj.service.dao.ConversationDao; import com.zbkj.service.dao.ConversationDao;
import com.zbkj.service.dao.PrivateMessageDao; import com.zbkj.service.dao.PrivateMessageDao;
import com.zbkj.service.dao.PrivateMessageBurnDao;
import com.zbkj.service.dao.UserBlacklistDao; import com.zbkj.service.dao.UserBlacklistDao;
import com.zbkj.service.service.ConversationService; import com.zbkj.service.service.ConversationService;
import com.zbkj.service.service.OnlineStatusService; import com.zbkj.service.service.OnlineStatusService;
@ -36,6 +39,9 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
@Autowired @Autowired
private PrivateMessageDao privateMessageDao; private PrivateMessageDao privateMessageDao;
@Autowired
private PrivateMessageBurnDao privateMessageBurnDao;
@Autowired @Autowired
private UserService userService; private UserService userService;
@ -189,19 +195,35 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
// 检查黑名单状态 // 检查黑名单状态
checkBlacklistStatus(userId, receiverId); checkBlacklistStatus(userId, receiverId);
String messageType = request.getMessageType() != null ? request.getMessageType() : "text";
// 获取消息内容兼容新旧字段 // 获取消息内容兼容新旧字段
String content = request.getContent() != null ? request.getContent() : request.getMessage(); String content = request.getContent() != null ? request.getContent() : request.getMessage();
// 文本消息要求 content 非空媒体消息允许 content 为空
if ("text".equals(messageType)) {
if (content == null || content.trim().isEmpty()) { if (content == null || content.trim().isEmpty()) {
throw new CrmebException("消息内容不能为空"); throw new CrmebException("消息内容不能为空");
} }
}
if ("burn_image".equals(messageType)) {
if (request.getMediaUrl() == null || request.getMediaUrl().trim().isEmpty()) {
throw new CrmebException("阅后图片mediaUrl不能为空");
}
Integer burnSeconds = request.getBurnSeconds();
if (burnSeconds == null || !(burnSeconds == 3 || burnSeconds == 5 || burnSeconds == 10)) {
throw new CrmebException("阅后即焚秒数仅支持3/5/10");
}
}
// 创建消息 // 创建消息
PrivateMessage message = new PrivateMessage(); PrivateMessage message = new PrivateMessage();
message.setConversationId(conversationId); message.setConversationId(conversationId);
message.setSenderId(userId); message.setSenderId(userId);
message.setReceiverId(receiverId); message.setReceiverId(receiverId);
message.setContent(content); message.setContent(content != null ? content : "");
message.setMessageType(request.getMessageType() != null ? request.getMessageType() : "text"); message.setMessageType(messageType);
message.setMediaUrl(request.getMediaUrl()); message.setMediaUrl(request.getMediaUrl());
message.setDuration(request.getDuration()); message.setDuration(request.getDuration());
message.setStatus("sent"); message.setStatus("sent");
@ -210,12 +232,25 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
message.setCreateTime(new Date()); message.setCreateTime(new Date());
privateMessageDao.insert(message); privateMessageDao.insert(message);
if ("burn_image".equals(messageType)) {
PrivateMessageBurn burn = new PrivateMessageBurn();
burn.setMessageId(message.getId());
burn.setConversationId(conversationId);
burn.setBurnSeconds(request.getBurnSeconds());
burn.setBurned(false);
burn.setCreateTime(new Date());
burn.setUpdateTime(new Date());
privateMessageBurnDao.insert(burn);
}
// 更新会话的最后消息和时间 // 更新会话的最后消息和时间
String lastMessagePreview = content; String lastMessagePreview = content;
if ("image".equals(message.getMessageType())) { if ("image".equals(message.getMessageType())) {
lastMessagePreview = "[图片]"; lastMessagePreview = "[图片]";
} else if ("voice".equals(message.getMessageType())) { } else if ("voice".equals(message.getMessageType())) {
lastMessagePreview = "[语音]"; lastMessagePreview = "[语音]";
} else if ("burn_image".equals(message.getMessageType())) {
lastMessagePreview = "[阅后图片]";
} }
LambdaUpdateWrapper<Conversation> uw = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<Conversation> uw = new LambdaUpdateWrapper<>();
@ -236,6 +271,84 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
return convertMessageToResponse(message); return convertMessageToResponse(message);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public BurnViewResponse viewBurnImage(Long messageId, Integer userId) {
PrivateMessage message = privateMessageDao.selectById(messageId);
if (message == null) {
throw new CrmebException("消息不存在");
}
if (!"burn_image".equals(message.getMessageType())) {
throw new CrmebException("消息类型不支持");
}
Conversation conversation = getById(message.getConversationId());
if (conversation == null ||
(!conversation.getUser1Id().equals(userId) && !conversation.getUser2Id().equals(userId))) {
throw new CrmebException("无权限查看此消息");
}
PrivateMessageBurn burn = privateMessageBurnDao.selectOne(
new LambdaQueryWrapper<PrivateMessageBurn>()
.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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean deleteMessage(Long messageId, Integer userId) { public Boolean deleteMessage(Long messageId, Integer userId) {
@ -395,6 +508,26 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
return response; return response;
} }
private void fillBurnFields(ChatMessageResponse response, Long messageId) {
PrivateMessageBurn burn = privateMessageBurnDao.selectOne(
new LambdaQueryWrapper<PrivateMessageBurn>()
.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);
}
/** /**
* 转换消息列表为响应对象列表 * 转换消息列表为响应对象列表
*/ */
@ -426,6 +559,11 @@ public class ConversationServiceImpl extends ServiceImpl<ConversationDao, Conver
response.setMediaUrl(message.getMediaUrl()); response.setMediaUrl(message.getMediaUrl());
response.setDuration(message.getDuration()); response.setDuration(message.getDuration());
if ("burn_image".equals(message.getMessageType())) {
response.setMediaUrl(null);
fillBurnFields(response, message.getId());
}
// 设置是否已读 // 设置是否已读
response.setIsRead("read".equals(message.getStatus())); response.setIsRead("read".equals(message.getStatus()));

View File

@ -29,7 +29,7 @@ public class RemoteUploadServiceImpl {
private static final Logger logger = LoggerFactory.getLogger(RemoteUploadServiceImpl.class); private static final Logger logger = LoggerFactory.getLogger(RemoteUploadServiceImpl.class);
@Value("${file.upload.server.url:http://1.15.149.240:30005/upload}") @Value("${file.upload.server.url:http://1.15.149.240:30005/api/upload}")
private String uploadServerUrl; private String uploadServerUrl;
private RestTemplate restTemplate; private RestTemplate restTemplate;

View File

@ -6,8 +6,6 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zbkj.common.exception.CrmebException; import com.zbkj.common.exception.CrmebException;
import com.zbkj.common.model.category.Category;
import com.zbkj.common.model.live.LiveRoomCategory;
import com.zbkj.common.model.user.User; import com.zbkj.common.model.user.User;
import com.zbkj.common.model.works.Works; import com.zbkj.common.model.works.Works;
import com.zbkj.common.page.CommonPage; import com.zbkj.common.page.CommonPage;
@ -15,7 +13,6 @@ import com.zbkj.common.request.WorksRequest;
import com.zbkj.common.request.WorksSearchRequest; import com.zbkj.common.request.WorksSearchRequest;
import com.zbkj.common.response.WorksResponse; import com.zbkj.common.response.WorksResponse;
import com.zbkj.service.dao.WorksDao; import com.zbkj.service.dao.WorksDao;
import com.zbkj.service.service.CategoryService;
import com.zbkj.service.service.LiveRoomCategoryService; import com.zbkj.service.service.LiveRoomCategoryService;
import com.zbkj.service.service.UserService; import com.zbkj.service.service.UserService;
import com.zbkj.service.service.WorksLikeService; import com.zbkj.service.service.WorksLikeService;
@ -41,9 +38,6 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
@Autowired @Autowired
private UserService userService; private UserService userService;
@Autowired
private CategoryService categoryService;
@Autowired @Autowired
private LiveRoomCategoryService liveRoomCategoryService; private LiveRoomCategoryService liveRoomCategoryService;
@ -80,10 +74,10 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
throw new CrmebException("作品类型只能是IMAGE或VIDEO"); throw new CrmebException("作品类型只能是IMAGE或VIDEO");
} }
// 验证分类是否存在如果提供了分类ID且不为0 // 验证分类是否存在如果提供了分类ID
if (request.getCategoryId() != null && request.getCategoryId() > 0) { if (request.getCategoryId() != null) {
LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId());
if (category == null || category.getStatus() != 1) { if (category == null || category.getStatus() == null || category.getStatus() != 1) {
throw new CrmebException("分类不存在或已禁用"); throw new CrmebException("分类不存在或已禁用");
} }
} }
@ -154,6 +148,21 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean updateWorks(WorksRequest request, Integer userId) { 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) { if (request.getId() == null) {
throw new CrmebException("作品ID不能为空"); throw new CrmebException("作品ID不能为空");
} }
@ -164,15 +173,22 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
throw new CrmebException("作品不存在"); throw new CrmebException("作品不存在");
} }
// 管理端userId=1可以编辑所有作品普通用户只能编辑自己的作品 log.info("原作品信息:");
if (userId != 1 && !works.getUid().equals(userId)) { log.info(" 原标题: {}", works.getTitle());
log.info(" 原描述: {}", works.getDescription());
log.info(" 原封面: {}", works.getCoverImage());
log.info(" 原图片: {}", works.getImages());
log.info(" 原视频: {}", works.getVideoUrl());
// 验证是否是作品作者
if (!works.getUid().equals(userId)) {
throw new CrmebException("无权限编辑此作品"); throw new CrmebException("无权限编辑此作品");
} }
// 验证分类是否存在如果提供了分类ID且不为0 // 验证分类是否存在如果提供了分类ID
if (request.getCategoryId() != null && request.getCategoryId() > 0) { if (request.getCategoryId() != null) {
LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId());
if (category == null || category.getStatus() != 1) { if (category == null || category.getStatus() == null || category.getStatus() != 1) {
throw new CrmebException("分类不存在或已禁用"); throw new CrmebException("分类不存在或已禁用");
} }
} }
@ -194,11 +210,63 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
works.setStatus(request.getStatus()); 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); boolean updated = updateById(works);
if (!updated) { if (!updated) {
throw new CrmebException("更新作品失败"); 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()); log.info("用户{}更新作品成功作品ID{}", userId, works.getId());
return true; return true;
} }
@ -212,8 +280,8 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
throw new CrmebException("作品不存在"); throw new CrmebException("作品不存在");
} }
// 管理端userId=1可以删除所有作品普通用户只能删除自己的作品 // 验证是否是作品作者
if (userId != 1 && !works.getUid().equals(userId)) { if (!works.getUid().equals(userId)) {
throw new CrmebException("无权限删除此作品"); throw new CrmebException("无权限删除此作品");
} }
@ -236,7 +304,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
public Boolean updateWorksStatus(Long worksId, Integer status) { public Boolean updateWorksStatus(Long worksId, Integer status) {
// 验证状态值 // 验证状态值
if (status != 0 && status != 1) { if (status != 0 && status != 1) {
throw new CrmebException("状态值只能是0下架或1上架"); throw new CrmebException("状态值无效,只能是0下架或1上架");
} }
// 查询作品 // 查询作品
@ -319,8 +387,10 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
// 状态筛选 // 状态筛选
if (request.getStatus() != null) { if (request.getStatus() != null) {
queryWrapper.eq(Works::getStatus, request.getStatus()); queryWrapper.eq(Works::getStatus, request.getStatus());
} else {
// 默认只查询正常状态的作品
queryWrapper.eq(Works::getStatus, 1);
} }
// 管理端不过滤状态可以看到所有作品包括已下架的
// 排序 // 排序
String orderBy = request.getOrderBy(); String orderBy = request.getOrderBy();
@ -452,7 +522,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
// 获取分类信息 // 获取分类信息
if (works.getCategoryId() != null) { if (works.getCategoryId() != null) {
LiveRoomCategory category = liveRoomCategoryService.getById(works.getCategoryId()); com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(works.getCategoryId());
if (category != null) { if (category != null) {
response.setCategoryName(category.getName()); response.setCategoryName(category.getName());
} }

View File

@ -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='阅后即焚消息状态表';

View File

@ -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;

View File

@ -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 = '作品表 - 支持单分类选择';

View File

@ -101,6 +101,16 @@
android:name="com.example.livestreaming.LikedRoomsActivity" android:name="com.example.livestreaming.LikedRoomsActivity"
android:exported="false" /> android:exported="false" />
<activity
android:name="com.example.livestreaming.MyLikesActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name="com.example.livestreaming.MyCollectionsActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity <activity
android:name="com.example.livestreaming.LikesListActivity" android:name="com.example.livestreaming.LikesListActivity"
android:exported="false" /> android:exported="false" />

View File

@ -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<ChannelManagerAdapter.ViewHolder> {
public interface OnChannelClickListener {
void onChannelClick(ChannelItem item, int position);
void onChannelDelete(ChannelItem item, int position);
void onChannelAdd(ChannelItem item, int position);
}
private final List<ChannelItem> 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<ChannelItem> newItems) {
items.clear();
if (newItems != null) {
items.addAll(newItems);
}
notifyDataSetChanged();
}
public List<ChannelItem> 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; }
}
}

View File

@ -71,32 +71,54 @@ public class ChannelTagAdapter extends ListAdapter<ChannelTagAdapter.ChannelTag,
class ViewHolder extends RecyclerView.ViewHolder { class ViewHolder extends RecyclerView.ViewHolder {
private final TextView tagText; private final TextView tagText;
private final View deleteIcon;
ViewHolder(@NonNull View itemView) { ViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);
tagText = itemView.findViewById(R.id.channelName); tagText = itemView.findViewById(R.id.channelName);
deleteIcon = itemView.findViewById(R.id.deleteIcon);
} }
void bind(ChannelTag tag, int position) { void bind(ChannelTag tag, int position) {
if (tag == null || tag.getName() == null) {
return;
}
try {
// 显示文本 // 显示文本
if (isRecommendMode) { if (isRecommendMode) {
tagText.setText("+ " + tag.getName()); tagText.setText("+ " + tag.getName());
deleteIcon.setVisibility(View.GONE);
} else { } else {
tagText.setText(tag.getName()); tagText.setText(tag.getName());
// 我的频道模式下除了"推荐"外都可以删除
if (position == 0 && "推荐".equals(tag.getName())) {
deleteIcon.setVisibility(View.GONE);
} else {
deleteIcon.setVisibility(View.VISIBLE);
}
} }
// 选中状态 // 选中状态样式 - 更明显的标签样式
boolean isSelected = position == selectedPosition; boolean isSelected = position == selectedPosition;
if (isSelected) { if (isSelected) {
// 选中状态蓝色背景白色文字
tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected); tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected);
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.black, null)); tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.white, null));
} else { } else {
tagText.setBackgroundResource(R.drawable.bg_channel_tag); 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.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null));
} }
}
// 点击事件 // 标签点击事件
itemView.setOnClickListener(v -> { tagText.setOnClickListener(v -> {
if (listener != null) { if (listener != null) {
if (isRecommendMode) { if (isRecommendMode) {
listener.onTagAddClick(tag, position); listener.onTagAddClick(tag, position);
@ -105,6 +127,16 @@ public class ChannelTagAdapter extends ListAdapter<ChannelTagAdapter.ChannelTag,
} }
} }
}); });
// 删除按钮点击事件
deleteIcon.setOnClickListener(v -> {
if (listener != null && !isRecommendMode) {
listener.onTagClick(tag, position);
}
});
} catch (Exception e) {
android.util.Log.e("ChannelTagAdapter", "绑定标签数据失败", e);
}
} }
} }

View File

@ -17,6 +17,7 @@ import android.speech.SpeechRecognizer;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
@ -44,6 +45,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse; import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.ApiService;
import com.example.livestreaming.net.CommunityResponse; import com.example.livestreaming.net.CommunityResponse;
import com.example.livestreaming.net.ConversationResponse; import com.example.livestreaming.net.ConversationResponse;
import com.example.livestreaming.net.CreateRoomRequest; 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_FANS, "粉丝", "关注你的人", R.drawable.ic_people_24));
// items.add(new DrawerCardItem(DrawerCardItem.ACTION_LIKES, "获赞", "收到的点赞", R.drawable.ic_heart_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_SEARCH, "搜索", "找主播/房间/标签", R.drawable.ic_search_24));
items.add(new DrawerCardItem(DrawerCardItem.ACTION_SETTINGS, "设置", "账号、隐私、通知", R.drawable.ic_menu_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)); 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 { try {
View notificationIcon = findViewById(R.id.notificationIcon); View notificationIcon = findViewById(R.id.notificationIcon);
@ -1759,6 +1773,9 @@ public class MainActivity extends AppCompatActivity {
private void loadCategoriesFromBackend() { private void loadCategoriesFromBackend() {
Log.d(TAG, "loadCategoriesFromBackend() 开始加载直播间分类"); Log.d(TAG, "loadCategoriesFromBackend() 开始加载直播间分类");
// 先从本地加载我的频道配置
loadMyChannelsFromPrefs();
// 调用后端接口获取直播间分类列表 // 调用后端接口获取直播间分类列表
ApiClient.getService(getApplicationContext()).getLiveRoomCategories() ApiClient.getService(getApplicationContext()).getLiveRoomCategories()
.enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>>() { .enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>>() {
@ -1775,8 +1792,11 @@ public class MainActivity extends AppCompatActivity {
if (categories != null && !categories.isEmpty()) { if (categories != null && !categories.isEmpty()) {
Log.d(TAG, "loadCategoriesFromBackend() 成功获取 " + categories.size() + " 个分类"); Log.d(TAG, "loadCategoriesFromBackend() 成功获取 " + categories.size() + " 个分类");
// 更新分类标签 // 缓存后端分类数据
updateCategoryTabs(categories); allBackendCategories.clear();
allBackendCategories.addAll(categories);
// 使用我的频道配置更新分类标签
updateCategoryTabsFromMyChannels();
} else { } else {
Log.w(TAG, "loadCategoriesFromBackend() 未获取到分类数据,使用默认分类"); Log.w(TAG, "loadCategoriesFromBackend() 未获取到分类数据,使用默认分类");
// 使用默认分类 // 使用默认分类
@ -1831,32 +1851,14 @@ public class MainActivity extends AppCompatActivity {
private void useDefaultCategories() { private void useDefaultCategories() {
if (binding == null || binding.categoryTabs == null) return; if (binding == null || binding.categoryTabs == null) return;
// 如果我的频道配置为空初始化默认配置
if (myChannels.isEmpty()) {
initDefaultMyChannels();
}
runOnUiThread(() -> { runOnUiThread(() -> {
// 清空现有标签 // 使用我的频道配置更新分类标签
binding.categoryTabs.removeAllTabs(); updateCategoryTabsFromMyChannels();
// 使用后端分类如果已加载
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();
Log.d(TAG, "useDefaultCategories() 使用默认分类,共 " + binding.categoryTabs.getTabCount() + " 个标签"); 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<ChannelManagerAdapter.ChannelItem> myChannelList = loadMyChannels();
myAdapter.submitList(myChannelList);
// 加载推荐频道数据
List<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> loadMyChannels() {
List<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> loadRecommendChannels(List<ChannelManagerAdapter.ChannelItem> myChannels) {
// 获取已添加的频道名称
java.util.Set<String> addedNames = new java.util.HashSet<>();
for (ChannelManagerAdapter.ChannelItem item : myChannels) {
addedNames.add(item.getName());
}
List<ChannelManagerAdapter.ChannelItem> 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<ChannelManagerAdapter.ChannelItem> 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<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> call =
apiService.getLiveRoomCategories();
call.enqueue(new Callback<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> call,
Response<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> response) {
try {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<com.example.livestreaming.net.CategoryResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<com.example.livestreaming.net.CategoryResponse> categories = apiResponse.getData();
// 在主线程中更新UI
runOnUiThread(() -> {
try {
// 清空推荐频道
recommendChannels.clear();
// 获取我的频道中已有的分类ID
Set<Integer> 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<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> call, Throwable t) {
Log.e(TAG, "加载分类失败", t);
// 如果API调用失败使用默认的推荐频道
runOnUiThread(() -> initDefaultRecommendChannels());
}
});
}
/**
* 初始化默认的推荐频道
*/
private void initDefaultRecommendChannels() {
try {
recommendChannels.clear();
// 获取我的频道中已有的分类名称
Set<String> 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, "使用默认我的频道配置");
}
} }

View File

@ -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<WorksResponse> 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<ApiResponse<PageResponse<WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<WorksResponse> 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<ApiResponse<PageResponse<WorksResponse>>> 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<Room> 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<ApiResponse<PageResponse<Map<String, Object>>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<Map<String, Object>> 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<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show();
}
}
});
}
private List<Room> convertToRooms(List<Map<String, Object>> roomMaps) {
List<Room> rooms = new ArrayList<>();
for (Map<String, Object> 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);
}
}
}
}

View File

@ -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<WorksResponse> 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<ApiResponse<PageResponse<WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<WorksResponse> 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<ApiResponse<PageResponse<WorksResponse>>> 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<Room> 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<ApiResponse<PageResponse<Map<String, Object>>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<Map<String, Object>> 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<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
isLoading = false;
hideLoading();
swipeRefreshLayout.setRefreshing(false);
if (getContext() != null) {
Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show();
}
}
});
}
private List<Room> convertToRooms(List<Map<String, Object>> roomMaps) {
List<Room> rooms = new ArrayList<>();
for (Map<String, Object> 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);
}
}
}
}

View File

@ -30,7 +30,9 @@ import com.example.livestreaming.ShareUtils;
import com.example.livestreaming.location.TianDiTuLocationService; import com.example.livestreaming.location.TianDiTuLocationService;
import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse; import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.UserInfoResponse; import com.example.livestreaming.net.UserInfoResponse;
import com.example.livestreaming.net.WorksResponse;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
@ -70,6 +72,7 @@ public class ProfileActivity extends AppCompatActivity {
private ActivityResultLauncher<Intent> editProfileLauncher; private ActivityResultLauncher<Intent> editProfileLauncher;
private UserWorksAdapter worksAdapter; private UserWorksAdapter worksAdapter;
private WorksAdapter myWorksAdapter;
public static void start(Context context) { public static void start(Context context) {
Intent intent = new Intent(context, ProfileActivity.class); Intent intent = new Intent(context, ProfileActivity.class);
@ -437,13 +440,19 @@ public class ProfileActivity extends AppCompatActivity {
startActivity(new Intent(this, FollowingActivity.class)); startActivity(new Intent(this, FollowingActivity.class));
}); });
binding.action2.setOnClickListener(v -> { binding.action2.setOnClickListener(v -> {
// 我的收藏点赞的直播间 // 我的点赞作品+直播间
if (!AuthHelper.requireLogin(this, "查看点赞需要登录")) {
return;
}
MyLikesActivity.start(this);
});
binding.action3.setOnClickListener(v -> {
// 我的收藏作品+直播间
if (!AuthHelper.requireLogin(this, "查看收藏需要登录")) { if (!AuthHelper.requireLogin(this, "查看收藏需要登录")) {
return; return;
} }
startActivity(new Intent(this, LikedRoomsActivity.class)); MyCollectionsActivity.start(this);
}); });
binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
binding.action4.setOnClickListener(v -> { binding.action4.setOnClickListener(v -> {
// 我的记录 - 跳转到统一记录页面 // 我的记录 - 跳转到统一记录页面
if (!AuthHelper.requireLogin(this, "查看记录需要登录")) { if (!AuthHelper.requireLogin(this, "查看记录需要登录")) {
@ -474,11 +483,11 @@ public class ProfileActivity extends AppCompatActivity {
}); });
binding.addFriendBtn.setOnClickListener(v -> { binding.addFriendBtn.setOnClickListener(v -> {
// 检查登录状态添加好友需要登录 // 我的挚友原添加好友功能已在挚友页面内
if (!AuthHelper.requireLogin(this, "添加好友需要登录")) { if (!AuthHelper.requireLogin(this, "查看挚友需要登录")) {
return; return;
} }
AddFriendActivity.start(this); startActivity(new Intent(this, MyFriendsActivity.class));
}); });
// 我的钱包按钮点击事件 // 我的钱包按钮点击事件
@ -554,46 +563,92 @@ public class ProfileActivity extends AppCompatActivity {
} }
private void setupWorksRecycler() { private void setupWorksRecycler() {
worksAdapter = new UserWorksAdapter(); // 设置我的作品区域
worksAdapter.setOnWorkClickListener(workItem -> { myWorksAdapter = new WorksAdapter(work -> {
if (workItem != null && !TextUtils.isEmpty(workItem.getId())) { 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.myWorksRecycler.setLayoutManager(new GridLayoutManager(this, 2));
binding.worksRecycler.setAdapter(worksAdapter); binding.myWorksRecycler.setAdapter(myWorksAdapter);
loadWorks();
// 发布按钮点击事件
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<ApiResponse<PageResponse<WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<WorksResponse> pageData = response.body().getData();
if (pageData != null && pageData.getList() != null && !pageData.getList().isEmpty()) {
List<WorksResponse> 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<ApiResponse<PageResponse<WorksResponse>>> 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() { private void loadWorks() {
// TODO: 接入后端接口 - 获取当前用户的作品列表 // 旧方法保留兼容实际使用loadMyWorks
// 接口路径: GET /api/users/{userId}/works loadMyWorks();
// 请求方法: GET
// 请求头: Authorization: Bearer {token} (必填从AuthStore获取)
// 路径参数:
// - userId: String (必填) - 当前用户ID从token中解析获取
// 请求参数Query:
// - page: int (可选默认1) - 页码
// - pageSize: int (可选默认20) - 每页数量
// 返回数据格式: ApiResponse<PageResult<WorkItem>>
// 实现步骤:
// 1. 从AuthStore获取token解析userId或从用户信息中获取
// 2. 调用接口获取作品列表
// 3. 更新UI显示作品列表或空状态
// 4. 处理错误情况网络错误未登录等
// 注意: 此方法在onResume时也会调用需要避免重复请求
// 临时从本地存储加载等待后端接口
List<WorkItem> 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);
}
} }
private void showTab(int index) { private void showTab(int index) {

View File

@ -83,6 +83,10 @@ public class PublishWorkActivity extends AppCompatActivity {
private String selectedVisibility = "所有人可见"; // 可见范围 private String selectedVisibility = "所有人可见"; // 可见范围
private String selectedCommentSetting = "所有人可评论"; // 评论设置 private String selectedCommentSetting = "所有人可评论"; // 评论设置
// 分类选择相关
private final List<com.example.livestreaming.net.CategoryResponse> allCategories = new ArrayList<>();
private com.example.livestreaming.net.CategoryResponse selectedCategory = null;
// 天地图定位服务继续 // 天地图定位服务继续
private TianDiTuLocationService locationService; private TianDiTuLocationService locationService;
private ActivityResultLauncher<String[]> requestLocationPermissionLauncher; private ActivityResultLauncher<String[]> requestLocationPermissionLauncher;
@ -116,6 +120,7 @@ public class PublishWorkActivity extends AppCompatActivity {
return; return;
} }
} }
binding = ActivityPublishWorkBinding.inflate(getLayoutInflater()); binding = ActivityPublishWorkBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot()); setContentView(binding.getRoot());
@ -126,6 +131,7 @@ public class PublishWorkActivity extends AppCompatActivity {
setupMediaAdapter(); setupMediaAdapter();
setupLaunchers(); setupLaunchers();
setupClickListeners(); setupClickListeners();
loadCategories(); // 加载分类数据
// 如果是编辑模式预填充数据 // 如果是编辑模式预填充数据
if (isEditMode) { if (isEditMode) {
@ -185,14 +191,6 @@ public class PublishWorkActivity extends AppCompatActivity {
// 更新UI显示 // 更新UI显示
updateMediaDisplay(); updateMediaDisplay();
updateCoverPreview(); updateCoverPreview();
// 更新按钮文本
binding.publishButton.setText("保存");
// 更新标题
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle("编辑作品");
}
} }
private void setupToolbar() { private void setupToolbar() {
@ -200,6 +198,14 @@ public class PublishWorkActivity extends AppCompatActivity {
if (getSupportActionBar() != null) { if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true);
// 根据模式设置标题
if (isEditMode) {
getSupportActionBar().setTitle("编辑作品");
binding.publishButton.setText("保存");
} else {
getSupportActionBar().setTitle("发布作品");
binding.publishButton.setText("发布");
}
} }
binding.toolbar.setNavigationOnClickListener(v -> finish()); binding.toolbar.setNavigationOnClickListener(v -> finish());
} }
@ -428,6 +434,9 @@ public class PublishWorkActivity extends AppCompatActivity {
binding.selectCoverButton.setOnClickListener(v -> showCoverPickerDialog()); binding.selectCoverButton.setOnClickListener(v -> showCoverPickerDialog());
binding.publishButton.setOnClickListener(v -> publishWork()); binding.publishButton.setOnClickListener(v -> publishWork());
// 分类选择点击事件
binding.categorySpinner.setOnClickListener(v -> showCategoryPickerDialog());
// 封面预览点击也可以选择封面 // 封面预览点击也可以选择封面
binding.coverPreview.setOnClickListener(v -> { binding.coverPreview.setOnClickListener(v -> {
showCoverPickerDialog(); showCoverPickerDialog();
@ -709,11 +718,29 @@ public class PublishWorkActivity extends AppCompatActivity {
} }
private void publishWork() { 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 String title = binding.titleEditText.getText() != null
? binding.titleEditText.getText().toString().trim() : ""; ? binding.titleEditText.getText().toString().trim() : "";
String description = binding.descriptionEditText.getText() != null String description = binding.descriptionEditText.getText() != null
? binding.descriptionEditText.getText().toString().trim() : ""; ? 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)) { if (TextUtils.isEmpty(title)) {
Toast.makeText(this, "请输入作品标题", Toast.LENGTH_SHORT).show(); 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); android.app.ProgressDialog progressDialog = new android.app.ProgressDialog(this);
progressDialog.setMessage("正在发布作品..."); progressDialog.setMessage(isEditMode ? "正在更新作品..." : "正在发布作品...");
progressDialog.setCancelable(false); progressDialog.setCancelable(false);
progressDialog.show(); progressDialog.show();
// 开始上传流程 // 开始上传流程
if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) { if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) {
// 视频作品先上传封面再上传视频最后发布 // 视频作品处理
uploadCoverImage(selectedCoverUri != null ? selectedCoverUri : selectedVideoUri, handleVideoWorkUpload(title, description, progressDialog);
new UploadCallback() { } 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 @Override
public void onSuccess(String url) { public void onSuccess(String coverUrl) {
String coverUrl = url; if (needUploadVideo) {
// 上传视频 // 需要上传视频
uploadVideo(selectedVideoUri, new UploadCallback() { uploadVideo(selectedVideoUri, new UploadCallback() {
@Override @Override
public void onSuccess(String videoUrl) { public void onSuccess(String videoUrl) {
// 发布作品
publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog); publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog);
} }
@ -774,6 +816,11 @@ public class PublishWorkActivity extends AppCompatActivity {
Toast.makeText(PublishWorkActivity.this, "视频上传失败: " + error, Toast.LENGTH_SHORT).show(); Toast.makeText(PublishWorkActivity.this, "视频上传失败: " + error, Toast.LENGTH_SHORT).show();
} }
}); });
} else {
// 使用原有视频URL
String videoUrl = selectedVideoUri.toString();
publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog);
}
} }
@Override @Override
@ -782,37 +829,139 @@ public class PublishWorkActivity extends AppCompatActivity {
Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show(); Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show();
} }
}); });
} else { } else if (needUploadVideo) {
// 图片作品先上传封面再上传所有图片最后发布 // 只需要上传视频使用原有封面
Uri coverUri = selectedCoverUri != null ? selectedCoverUri : String coverUrl = selectedCoverUri != null ? selectedCoverUri.toString() : selectedVideoUri.toString();
(!selectedMediaUris.isEmpty() ? selectedMediaUris.get(0) : null); uploadVideo(selectedVideoUri, new UploadCallback() {
uploadCoverImage(coverUri, new UploadCallback() {
@Override @Override
public void onSuccess(String coverUrl) { public void onSuccess(String videoUrl) {
// 上传所有图片 publishWorkToServer(title, description, "VIDEO", coverUrl, videoUrl, null, progressDialog);
uploadImages(selectedMediaUris, new UploadImagesCallback() {
@Override
public void onSuccess(List<String> imageUrls) {
// 发布作品
publishWorkToServer(title, description, "IMAGE", coverUrl, null, imageUrls, progressDialog);
} }
@Override @Override
public void onFailure(String error) { 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<Uri> localImageUris = new ArrayList<>();
List<String> 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<String> newImageUrls) {
android.util.Log.d("PublishWork", "新图片上传成功,数量: " + newImageUrls.size());
// 合并新上传的图片URL和原有的图片URL
List<String> 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(); progressDialog.dismiss();
Toast.makeText(PublishWorkActivity.this, "图片上传失败: " + error, Toast.LENGTH_SHORT).show(); Toast.makeText(PublishWorkActivity.this, "图片上传失败: " + error, Toast.LENGTH_SHORT).show();
} }
}); });
} else {
android.util.Log.d("PublishWork", "无新图片需要上传,使用原有图片");
// 只使用原有图片
publishWorkToServer(title, description, "IMAGE", coverUrl, null, existingImageUrls, progressDialog);
}
} }
@Override @Override
public void onFailure(String error) { public void onFailure(String error) {
android.util.Log.e("PublishWork", "封面上传失败: " + error);
progressDialog.dismiss(); progressDialog.dismiss();
Toast.makeText(PublishWorkActivity.this, "封面上传失败: " + error, Toast.LENGTH_SHORT).show(); 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<String> newImageUrls) {
android.util.Log.d("PublishWork", "新图片上传成功,数量: " + newImageUrls.size());
List<String> 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, private void publishWorkToServer(String title, String description, String type,
String coverUrl, String videoUrl, List<String> imageUrls, String coverUrl, String videoUrl, List<String> imageUrls,
android.app.ProgressDialog progressDialog) { android.app.ProgressDialog progressDialog) {
WorksRequest request = new WorksRequest(); 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 // 如果是编辑模式设置作品ID
if (isEditMode && !TextUtils.isEmpty(editWorkId)) { 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"; // 默认所有人可见 String visibilityValue = "PUBLIC"; // 默认所有人可见
if ("所有人可见".equals(selectedVisibility)) { if ("所有人可见".equals(selectedVisibility)) {
@ -1057,30 +1207,35 @@ public class PublishWorkActivity extends AppCompatActivity {
} }
request.setCommentSetting(commentSettingValue); request.setCommentSetting(commentSettingValue);
// 设置分类ID
if (selectedCategory != null) {
request.setCategoryId(selectedCategory.getId());
}
ApiService apiService = ApiClient.getService(this); ApiService apiService = ApiClient.getService(this);
// 根据模式选择不同的API调用
if (isEditMode) { if (isEditMode) {
// 编辑模式调用更新接口 // 编辑模式调用更新API
Call<ApiResponse<Boolean>> call = apiService.updateWork(request); Call<ApiResponse<Boolean>> updateCall = apiService.updateWork(request);
call.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() { updateCall.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
@Override @Override
public void onResponse(Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) { public void onResponse(Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) {
progressDialog.dismiss(); progressDialog.dismiss();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
ApiResponse<Boolean> apiResponse = response.body(); ApiResponse<Boolean> apiResponse = response.body();
if (apiResponse.getCode() == 200 && Boolean.TRUE.equals(apiResponse.getData())) { if (apiResponse.getCode() == 200) {
Toast.makeText(PublishWorkActivity.this, "保存成功", Toast.LENGTH_SHORT).show(); Toast.makeText(PublishWorkActivity.this, "更新成功", Toast.LENGTH_SHORT).show();
// 设置结果并返回
setResult(RESULT_OK); setResult(RESULT_OK);
finish(); finish();
} else { } else {
Toast.makeText(PublishWorkActivity.this, String errorMessage = apiResponse.getMessage() != null ? apiResponse.getMessage() : "更新失败";
apiResponse.getMessage() != null ? apiResponse.getMessage() : "保存失败", Toast.makeText(PublishWorkActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show();
} }
} else { } 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 { } else {
// 发布模式调用发布接口 // 发布模式调用发布API
Call<ApiResponse<Long>> call = apiService.publishWork(request); Call<ApiResponse<Long>> publishCall = apiService.publishWork(request);
call.enqueue(new retrofit2.Callback<ApiResponse<Long>>() { publishCall.enqueue(new retrofit2.Callback<ApiResponse<Long>>() {
@Override @Override
public void onResponse(Call<ApiResponse<Long>> call, retrofit2.Response<ApiResponse<Long>> response) { public void onResponse(Call<ApiResponse<Long>> call, retrofit2.Response<ApiResponse<Long>> response) {
progressDialog.dismiss(); progressDialog.dismiss();
@ -1102,11 +1257,13 @@ public class PublishWorkActivity extends AppCompatActivity {
ApiResponse<Long> apiResponse = response.body(); ApiResponse<Long> apiResponse = response.body();
if (apiResponse.getCode() == 200) { if (apiResponse.getCode() == 200) {
Toast.makeText(PublishWorkActivity.this, "发布成功", Toast.LENGTH_SHORT).show(); Toast.makeText(PublishWorkActivity.this, "发布成功", Toast.LENGTH_SHORT).show();
// 设置结果并返回
setResult(RESULT_OK);
finish(); finish();
} else { } else {
Toast.makeText(PublishWorkActivity.this, String errorMessage = apiResponse.getMessage() != null ? apiResponse.getMessage() : "发布失败";
apiResponse.getMessage() != null ? apiResponse.getMessage() : "发布失败", Toast.makeText(PublishWorkActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
Toast.LENGTH_SHORT).show();
} }
} else { } else {
Toast.makeText(PublishWorkActivity.this, "发布失败", Toast.LENGTH_SHORT).show(); 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<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> call =
apiService.getLiveRoomCategories();
call.enqueue(new retrofit2.Callback<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> call,
retrofit2.Response<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> response) {
try {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<List<com.example.livestreaming.net.CategoryResponse>> 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<ApiResponse<List<com.example.livestreaming.net.CategoryResponse>>> 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());
}
}
}

View File

@ -440,26 +440,16 @@ public class RoomDetailActivity extends AppCompatActivity implements SurfaceHold
// 加载点赞数 // 加载点赞数
loadLikeCount(); loadLikeCount();
// 点赞按钮点击事件 // 点赞按钮点击事件 - TRTC风格动画
likeButton.setOnClickListener(v -> { likeButton.setOnClickListener(v -> {
if (!AuthHelper.isLoggedIn(this)) { if (!AuthHelper.isLoggedIn(this)) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
return; return;
} }
// 点赞动画 // TRTC风格点赞动画
likeButton.animate() android.view.animation.Animation likeAnim = android.view.animation.AnimationUtils.loadAnimation(this, R.anim.trtc_like_animation);
.scaleX(1.3f) likeButton.startAnimation(likeAnim);
.scaleY(1.3f)
.setDuration(100)
.withEndAction(() -> {
likeButton.animate()
.scaleX(1.0f)
.scaleY(1.0f)
.setDuration(100)
.start();
})
.start();
// 调用点赞API // 调用点赞API
likeRoom(); likeRoom();

View File

@ -23,6 +23,8 @@ import com.example.livestreaming.net.HotSearchResponse;
import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.PageResponse;
import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.Room; 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.flexbox.FlexboxLayout;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
@ -48,9 +50,13 @@ public class SearchActivity extends AppCompatActivity {
private SearchStreamerAdapter streamersAdapter; private SearchStreamerAdapter streamersAdapter;
private final List<Map<String, Object>> streamersList = new ArrayList<>(); private final List<Map<String, Object>> streamersList = new ArrayList<>();
// 作品列表
private WorksAdapter worksAdapter;
private final List<WorksResponse> worksList = new ArrayList<>();
private boolean isSearching = false; private boolean isSearching = false;
private String lastSearchKeyword = ""; 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"; private static final String EXTRA_SEARCH_QUERY = "search_query";
@ -118,11 +124,11 @@ public class SearchActivity extends AppCompatActivity {
streamersAdapter.setOnStreamerClickListener(new SearchStreamerAdapter.OnStreamerClickListener() { streamersAdapter.setOnStreamerClickListener(new SearchStreamerAdapter.OnStreamerClickListener() {
@Override @Override
public void onStreamerClick(Map<String, Object> streamer) { public void onStreamerClick(Map<String, Object> streamer) {
// 点击主播跳转到主播主页 // 点击用户跳转到用户主页
Object id = streamer.get("id"); Object id = streamer.get("id");
if (id != null) { if (id != null) {
int streamerId = ((Number) id).intValue(); int userId = ((Number) id).intValue();
UserProfileActivity.start(SearchActivity.this, streamerId); UserProfileReadOnlyActivity.start(SearchActivity.this, userId);
} }
} }
@ -135,12 +141,24 @@ public class SearchActivity extends AppCompatActivity {
binding.streamersRecyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.streamersRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.streamersRecyclerView.setAdapter(streamersAdapter); 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() { private void setupTabs() {
// 添加Tab // 添加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.addTab(binding.searchTabs.newTab().setText("作品"));
binding.searchTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { binding.searchTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override @Override
@ -216,6 +234,149 @@ public class SearchActivity extends AppCompatActivity {
binding.hotSearchContainer.setVisibility(View.GONE); binding.hotSearchContainer.setVisibility(View.GONE);
binding.emptyStateView.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<ApiResponse<Map<String, Object>>> roomsCall =
apiService.comprehensiveSearch(searchKeyword, 1, 20);
roomsCall.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Map<String, Object> data = apiResponse.getData();
// 解析直播间列表
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<String, Object>) 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<String, Object>) item);
}
}
}
}
}
completedCount[0]++;
if (completedCount[0] >= totalRequests) {
onSearchCompleted();
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
Log.e(TAG, "搜索直播间和用户失败", t);
completedCount[0]++;
if (completedCount[0] >= totalRequests) {
onSearchCompleted();
}
}
});
// 搜索作品
Map<String, Object> worksRequest = new HashMap<>();
worksRequest.put("keyword", searchKeyword);
worksRequest.put("page", 1);
worksRequest.put("pageSize", 20);
Call<ApiResponse<PageResponse<WorksResponse>>> worksCall =
apiService.searchWorks(worksRequest);
worksCall.enqueue(new Callback<ApiResponse<PageResponse<WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<PageResponse<WorksResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<WorksResponse> works = apiResponse.getData().getList();
if (works != null) {
worksList.addAll(works);
}
}
}
completedCount[0]++;
if (completedCount[0] >= totalRequests) {
onSearchCompleted();
}
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<WorksResponse>>> 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); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = Call<ApiResponse<Map<String, Object>>> call =
apiService.comprehensiveSearch(keyword, 1, 20); apiService.comprehensiveSearch(keyword, 1, 20);
@ -263,11 +424,12 @@ public class SearchActivity extends AppCompatActivity {
// 获取总数 // 获取总数
int roomsTotal = getIntValue(data.get("roomsTotal"), 0); int roomsTotal = getIntValue(data.get("roomsTotal"), 0);
int streamersTotal = getIntValue(data.get("streamersTotal"), 0); int streamersTotal = getIntValue(data.get("streamersTotal"), 0);
int worksTotal = 0; // 旧方法没有作品数据
Log.d(TAG, "搜索成功,直播间: " + roomsTotal + ", 主播: " + streamersTotal); Log.d(TAG, "搜索成功,直播间: " + roomsTotal + ", 主播: " + streamersTotal);
// 更新Tab标题显示数量 // 更新Tab标题显示数量
updateTabTitles(roomsTotal, streamersTotal); updateTabTitles(roomsTotal, streamersTotal, worksTotal);
// 显示搜索结果 // 显示搜索结果
showSearchResults(); showSearchResults();
@ -301,15 +463,19 @@ public class SearchActivity extends AppCompatActivity {
/** /**
* 更新Tab标题显示数量 * 更新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 roomsTab = binding.searchTabs.getTabAt(0);
TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1);
TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2);
if (roomsTab != null) { if (roomsTab != null) {
roomsTab.setText("直播间 " + roomsCount); roomsTab.setText("直播间 " + roomsCount);
} }
if (streamersTab != null) { 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)); roomsAdapter.submitList(new ArrayList<>(roomsList));
streamersAdapter.submitList(new ArrayList<>(streamersList)); streamersAdapter.submitList(new ArrayList<>(streamersList));
worksAdapter.submitList(new ArrayList<>(worksList));
// 根据当前Tab显示对应内容 // 根据当前Tab显示对应内容
updateTabContent(); updateTabContent();
@ -336,6 +503,7 @@ public class SearchActivity extends AppCompatActivity {
// 显示直播间 // 显示直播间
binding.roomsRecyclerView.setVisibility(View.VISIBLE); binding.roomsRecyclerView.setVisibility(View.VISIBLE);
binding.streamersRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.GONE);
binding.worksRecyclerView.setVisibility(View.GONE);
if (roomsList.isEmpty()) { if (roomsList.isEmpty()) {
binding.emptyStateView.setNoSearchResultsState(); binding.emptyStateView.setNoSearchResultsState();
@ -343,10 +511,11 @@ public class SearchActivity extends AppCompatActivity {
} else { } else {
binding.emptyStateView.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE);
} }
} else { } else if (currentTab == 1) {
// 显示主播 // 显示主播
binding.roomsRecyclerView.setVisibility(View.GONE); binding.roomsRecyclerView.setVisibility(View.GONE);
binding.streamersRecyclerView.setVisibility(View.VISIBLE); binding.streamersRecyclerView.setVisibility(View.VISIBLE);
binding.worksRecyclerView.setVisibility(View.GONE);
if (streamersList.isEmpty()) { if (streamersList.isEmpty()) {
binding.emptyStateView.setNoSearchResultsState(); binding.emptyStateView.setNoSearchResultsState();
@ -354,6 +523,18 @@ public class SearchActivity extends AppCompatActivity {
} else { } else {
binding.emptyStateView.setVisibility(View.GONE); 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.searchTabs.setVisibility(View.GONE);
binding.roomsRecyclerView.setVisibility(View.GONE); binding.roomsRecyclerView.setVisibility(View.GONE);
binding.streamersRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.GONE);
binding.worksRecyclerView.setVisibility(View.GONE);
binding.emptyStateView.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE);
binding.hotSearchContainer.setVisibility(View.VISIBLE); binding.hotSearchContainer.setVisibility(View.VISIBLE);
// 重置Tab标题 // 重置Tab标题
TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0); TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0);
TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1);
TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2);
if (roomsTab != null) roomsTab.setText("直播间"); 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<String, Object> streamer, int position) { private void handleFollowClick(Map<String, Object> streamer, int position) {
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) { if (!AuthHelper.requireLogin(this, "关注用户需要登录")) {
return; return;
} }
Object id = streamer.get("id"); Object id = streamer.get("id");
if (id == null) return; if (id == null) return;
int streamerId = ((Number) id).intValue(); int userId = ((Number) id).intValue();
Object isFollowing = streamer.get("isFollowing"); Object isFollowing = streamer.get("isFollowing");
boolean following = isFollowing instanceof Boolean && (Boolean) isFollowing; boolean following = isFollowing instanceof Boolean && (Boolean) isFollowing;
String action = following ? "unfollow" : "follow"; String action = following ? "unfollow" : "follow";
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
body.put("followedId", streamerId); body.put("userId", userId);
ApiService apiService = ApiClient.getService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Map<String, Object>>> call = following ? Call<ApiResponse<Map<String, Object>>> call = following ?

View File

@ -68,6 +68,15 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
context.startActivity(intent); 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -122,6 +131,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
// 检查关注状态 // 检查关注状态
checkFollowStatus(); checkFollowStatus();
// 初始化关注按钮显示默认显示"关注"
updateFollowButton();
// 检查好友状态 // 检查好友状态
checkFriendStatus(); checkFriendStatus();
@ -285,11 +297,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
if (isFollowing) { if (isFollowing) {
binding.followButton.setText("已关注"); binding.followButton.setText("已关注");
binding.followButton.setSelected(true); binding.followButton.setSelected(true);
binding.followButton.setBackgroundResource(R.drawable.bg_follow_button_followed);
binding.followButton.setTextColor(getResources().getColor(android.R.color.darker_gray, null)); binding.followButton.setTextColor(getResources().getColor(android.R.color.darker_gray, null));
} else { } else {
binding.followButton.setText("关注"); binding.followButton.setText("关注");
binding.followButton.setSelected(false); 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) { private void bindDemoStatsAndWorks(String userId) {
if (binding == null) return; 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<ApiResponse<Map<String, Object>>> call = apiService.getUserProfile(uid);
call.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Map<String, Object> data = apiResponse.getData();
// 更新用户名
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<ApiResponse<Map<String, Object>>> 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<ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call =
apiService.getUserWorks(uid, 1, 50);
call.enqueue(new Callback<ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> call,
Response<ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
List<com.example.livestreaming.net.WorksResponse> worksList = apiResponse.getData().getList();
if (worksList != null && !worksList.isEmpty()) {
// 转换为WorkItem列表
List<WorkItem> 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<ApiResponse<com.example.livestreaming.net.PageResponse<com.example.livestreaming.net.WorksResponse>>> 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 h = Math.abs(seed.hashCode());
int worksCount = 6 + (h % 18); int worksCount = 6 + (h % 18);
@ -517,16 +692,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
// 创建演示作品列表 // 创建演示作品列表
List<WorkItem> works = new ArrayList<>(); List<WorkItem> 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++) { for (int i = 0; i < 12; i++) {
WorkItem work = new WorkItem(); WorkItem work = new WorkItem();
work.setId("demo_" + userId + "_" + i); work.setId("demo_" + i);
work.setTitle("演示作品 " + (i + 1)); work.setTitle("演示作品 " + (i + 1));
work.setType(WorkItem.WorkType.IMAGE); work.setType(WorkItem.WorkType.IMAGE);
work.setLikeCount((h + i) % 100); 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";
}
}
} }

View File

@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import retrofit2.Call; import retrofit2.Call;
@ -44,6 +45,7 @@ public class WorkDetailActivity extends AppCompatActivity {
// 状态标记 // 状态标记
private boolean isLiked = false; private boolean isLiked = false;
private boolean isFavorited = false; private boolean isFavorited = false;
private boolean isFollowed = false; // 是否已关注作者
private int commentCount = 0; private int commentCount = 0;
private static final String EXTRA_WORK_ID = "work_id"; private static final String EXTRA_WORK_ID = "work_id";
@ -804,40 +806,213 @@ public class WorkDetailActivity extends AppCompatActivity {
} }
} }
private void setupActionButton() { /**
// ============================================ * 设置作者头像和关注按钮
// TODO: 判断是否是当前用户的作品 */
// ============================================ private void setupAuthorSection() {
// 判断方式推荐方式3: if (workItem == null) return;
// 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<IsOwnerResponse>
// 后端返回: { "code": 200, "data": { "isOwner": true/false } }
//
// 目前简化处理所有作品都显示操作按钮
// TODO: 根据isOwner字段控制按钮显示/隐藏
// 新布局中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<ApiResponse<Map<String, Object>>> call = apiService.checkFollowStatus(userId);
call.enqueue(new retrofit2.Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
retrofit2.Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> apiResponse = response.body();
if (apiResponse.getCode() == 200 && apiResponse.getData() != null) {
Object isFollowingObj = apiResponse.getData().get("isFollowing");
isFollowed = isFollowingObj != null && Boolean.parseBoolean(isFollowingObj.toString());
updateFollowButton();
}
}
}
@Override
public void onFailure(Call<ApiResponse<Map<String, Object>>> 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<String, Object> body = new java.util.HashMap<>();
body.put("userId", userId);
Call<ApiResponse<Map<String, Object>>> call;
if (isFollowed) {
// 取消关注
call = apiService.unfollowUser(body);
} else {
// 关注
call = apiService.followUser(body);
}
// 乐观更新UI
boolean oldFollowed = isFollowed;
isFollowed = !isFollowed;
updateFollowButton();
call.enqueue(new retrofit2.Callback<ApiResponse<Map<String, Object>>>() {
@Override
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
retrofit2.Response<ApiResponse<Map<String, Object>>> response) {
if (response.isSuccessful() && response.body() != null) {
ApiResponse<Map<String, Object>> 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<ApiResponse<Map<String, Object>>> 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() { binding.actionButton.setOnClickListener(new DebounceClickListener() {
@Override @Override
public void onDebouncedClick(View v) { public void onDebouncedClick(View v) {
@ -847,10 +1022,21 @@ public class WorkDetailActivity extends AppCompatActivity {
} }
private void showActionMenu() { private void showActionMenu() {
// TODO: 判断是否是当前用户的作品 // 检查是否是当前用户的作品
boolean isOwner = true; // 简化处理 String currentUserIdStr = AuthStore.getUserId(this);
if (currentUserIdStr == null || workItem == null) {
return;
}
if (!isOwner) { 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; return;
} }
@ -869,58 +1055,32 @@ public class WorkDetailActivity extends AppCompatActivity {
} }
private void editWork() { private void editWork() {
// ============================================ // 检查登录状态
// TODO: 实现编辑作品功能 if (!AuthHelper.requireLogin(this, "编辑作品需要登录")) {
// ============================================ return;
// 方案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<WorkItem>
//
// 前端需要传入的参数:
// - 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
Toast.makeText(this, "编辑功能待实现", Toast.LENGTH_SHORT).show(); 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());
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() { private void deleteWork() {
@ -1066,6 +1226,7 @@ public class WorkDetailActivity extends AppCompatActivity {
setupContent(); setupContent();
setupActionButtons(); setupActionButtons();
setupActionButton(); setupActionButton();
setupAuthorSection(); // 设置作者头像和关注按钮
// 获取真实的评论数量 // 获取真实的评论数量
loadRealCommentCount(worksResponse.getId()); loadRealCommentCount(worksResponse.getId());
@ -1114,6 +1275,11 @@ public class WorkDetailActivity extends AppCompatActivity {
// 防止 publishTime null 导致 NullPointerException // 防止 publishTime null 导致 NullPointerException
item.setPublishTime(response.getPublishTime() != null ? response.getPublishTime() : 0L); 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())) { if ("VIDEO".equals(response.getType())) {
item.setType(WorkItem.WorkType.VIDEO); item.setType(WorkItem.WorkType.VIDEO);
@ -1157,3 +1323,4 @@ public class WorkDetailActivity extends AppCompatActivity {
} }
} }
} }

View File

@ -23,6 +23,11 @@ public class WorkItem implements Parcelable {
private long publishTime; private long publishTime;
private WorkType type; // 作品类型图片或视频 private WorkType type; // 作品类型图片或视频
// 作者信息
private int authorId; // 作者用户ID
private String authorName; // 作者昵称
private String authorAvatar; // 作者头像URL
// 本地使用的URI发布时使用 // 本地使用的URI发布时使用
private transient Uri coverUri; private transient Uri coverUri;
private transient Uri videoUri; private transient Uri videoUri;
@ -41,6 +46,9 @@ public class WorkItem implements Parcelable {
this.publishTime = System.currentTimeMillis(); this.publishTime = System.currentTimeMillis();
this.imageUrls = new ArrayList<>(); this.imageUrls = new ArrayList<>();
this.imageUris = new ArrayList<>(); this.imageUris = new ArrayList<>();
this.authorId = 0;
this.authorName = "";
this.authorAvatar = "";
} }
public WorkItem(String id, String title, String description, String coverUrl, public WorkItem(String id, String title, String description, String coverUrl,
@ -171,6 +179,30 @@ public class WorkItem implements Parcelable {
this.imageUris = imageUris; 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 // Parcelable implementation
protected WorkItem(Parcel in) { protected WorkItem(Parcel in) {
id = in.readString(); id = in.readString();
@ -186,6 +218,9 @@ public class WorkItem implements Parcelable {
int typeOrdinal = in.readInt(); int typeOrdinal = in.readInt();
type = typeOrdinal >= 0 && typeOrdinal < WorkType.values().length type = typeOrdinal >= 0 && typeOrdinal < WorkType.values().length
? WorkType.values()[typeOrdinal] : WorkType.IMAGE; ? WorkType.values()[typeOrdinal] : WorkType.IMAGE;
authorId = in.readInt();
authorName = in.readString();
authorAvatar = in.readString();
} }
@Override @Override
@ -201,6 +236,9 @@ public class WorkItem implements Parcelable {
dest.writeInt(viewCount); dest.writeInt(viewCount);
dest.writeLong(publishTime); dest.writeLong(publishTime);
dest.writeInt(type != null ? type.ordinal() : -1); dest.writeInt(type != null ? type.ordinal() : -1);
dest.writeInt(authorId);
dest.writeString(authorName);
dest.writeString(authorAvatar);
} }
@Override @Override

View File

@ -380,7 +380,24 @@ public interface ApiService {
@GET("api/front/works/detail/{id}") @GET("api/front/works/detail/{id}")
Call<ApiResponse<WorksResponse>> getWorkDetail(@Path("id") long id); Call<ApiResponse<WorksResponse>> getWorkDetail(@Path("id") long id);
@DELETE("api/front/works/delete/{id}") /**
* 获取当前用户发布的作品列表
*/
@GET("api/front/works/my")
Call<ApiResponse<PageResponse<WorksResponse>>> getMyPublishedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取指定用户发布的作品列表
*/
@GET("api/front/works/user/{userId}")
Call<ApiResponse<PageResponse<WorksResponse>>> getUserWorks(
@Path("userId") int userId,
@Query("page") int page,
@Query("pageSize") int pageSize);
@POST("api/front/works/delete/{id}")
Call<ApiResponse<Boolean>> deleteWork(@Path("id") long id); Call<ApiResponse<Boolean>> deleteWork(@Path("id") long id);
@POST("api/front/works/like/{id}") @POST("api/front/works/like/{id}")
@ -983,31 +1000,27 @@ public interface ApiService {
@Query("page") int page, @Query("page") int page,
@Query("limit") int limit); @Query("limit") int limit);
// ==================== 聊天室接口 ==================== /**
* 搜索作品使用POST方法
*/
@POST("api/front/works/search")
Call<ApiResponse<PageResponse<WorksResponse>>> searchWorks(@Body Map<String, Object> request);
// ==================== 我的点赞/收藏作品接口 ====================
/** /**
* 获取聊天室列表 * 获取我点赞的作品列表
*/ */
@GET("api/front/chatroom/list") @GET("api/front/works/my/liked")
Call<ApiResponse<PageResponse<ChatRoomResponse>>> getChatRoomList( Call<ApiResponse<PageResponse<WorksResponse>>> getMyLikedWorks(
@Query("page") int page, @Query("page") int page,
@Query("limit") int limit); @Query("pageSize") int pageSize);
/** /**
* 获取聊天室详情 * 获取我收藏的作品列表
*/ */
@GET("api/front/chatroom/detail/{id}") @GET("api/front/works/my/collected")
Call<ApiResponse<ChatRoomResponse>> getChatRoomDetail(@Path("id") int id); Call<ApiResponse<PageResponse<WorksResponse>>> getMyCollectedWorks(
@Query("page") int page,
/** @Query("pageSize") int pageSize);
* 检查是否可以进入聊天室
*/
@GET("api/front/chatroom/check-entry/{roomId}")
Call<ApiResponse<Map<String, Object>>> checkChatRoomEntry(@Path("roomId") int roomId);
/**
* 支付进入聊天室
*/
@POST("api/front/chatroom/pay-entry/{roomId}")
Call<ApiResponse<Map<String, Object>>> payChatRoomEntry(@Path("roomId") int roomId);
} }

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="100"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.95"
android:toYScale="0.95" />
<scale
android:duration="100"
android:fromXScale="0.95"
android:fromYScale="0.95"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="100"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="150"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.3"
android:toYScale="1.3" />
<scale
android:duration="150"
android:fromXScale="1.3"
android:fromYScale="1.3"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="150"
android:toXScale="1.0"
android:toYScale="1.0" />
<alpha
android:duration="300"
android:fromAlpha="1.0"
android:toAlpha="0.8" />
<alpha
android:duration="200"
android:fromAlpha="0.8"
android:startOffset="300"
android:toAlpha="1.0" />
</set>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="16dp" />
<stroke <stroke
android:width="1dp" android:width="1dp"
android:color="#E0E0E0" /> android:color="#E0E0E0" />
<corners android:radius="4dp" />
<solid android:color="@android:color/white" />
</shape> </shape>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#F8F8F8" /> <solid android:color="#F5F5F5" />
<corners android:radius="4dp" /> <corners android:radius="16dp" />
<stroke <stroke
android:width="1dp" android:width="1dp"
android:color="#E8E8E8" /> android:color="#E0E0E0" />
</shape> </shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="#E0E0E0"
android:dashWidth="4dp"
android:dashGap="2dp" />
</shape>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#2196F3" />
<corners android:radius="16dp" />
<stroke <stroke
android:width="1dp" android:width="2dp"
android:color="#333333" /> android:color="#1976D2" />
<corners android:radius="4dp" />
<solid android:color="@android:color/white" />
</shape> </shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

View File

@ -1,19 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 已关注状态 --> android:shape="oval">
<item android:state_selected="true"> <solid android:color="#FF4757" />
<shape android:shape="rectangle"> <size
<solid android:color="#F5F5F5" /> android:width="20dp"
<corners android:radius="22dp" /> android:height="20dp" />
<stroke android:width="1dp" android:color="#DDDDDD" /> </shape>
</shape>
</item>
<!-- 未关注状态 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="22dp" />
<stroke android:width="1dp" android:color="#FF4757" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F5F5F5" />
<corners android:radius="22dp" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#CCCCCC" />
<corners android:radius="22dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/trtc_accent"/>
<corners android:radius="22dp"/>
<stroke android:width="1dp" android:color="@color/trtc_accent_light"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/trtc_overlay_dark"/>
<corners android:radius="22dp"/>
<stroke android:width="1dp" android:color="@color/trtc_overlay_light"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/trtc_overlay_dark"/>
<corners android:radius="16dp"/>
<stroke android:width="1dp" android:color="@color/trtc_overlay_light"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/trtc_overlay_light"/>
<corners android:radius="20dp"/>
<stroke android:width="1dp" android:color="@color/trtc_overlay_light"/>
</shape>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back_24"
app:title="我的收藏"
app:titleTextColor="@color/text_primary" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/white"
app:tabIndicatorColor="@color/purple_500"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="3dp"
app:tabMode="fixed"
app:tabGravity="fill"
app:tabSelectedTextColor="@color/purple_500"
app:tabTextColor="@color/text_secondary" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back_24"
app:title="我的点赞"
app:titleTextColor="@color/text_primary" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/white"
app:tabIndicatorColor="@color/purple_500"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="3dp"
app:tabMode="fixed"
app:tabGravity="fill"
app:tabSelectedTextColor="@color/purple_500"
app:tabTextColor="@color/text_secondary" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -70,6 +70,7 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintEnd_toStartOf="@id/topActionClock"
app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintStart_toStartOf="@id/name"
@ -187,6 +188,7 @@
android:background="@drawable/bg_circle_white_60" android:background="@drawable/bg_circle_white_60"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_clock_24" android:src="@drawable/ic_clock_24"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/topActionMore" app:layout_constraintEnd_toStartOf="@id/topActionMore"
app:layout_constraintTop_toTopOf="@id/topActionMore" /> app:layout_constraintTop_toTopOf="@id/topActionMore" />
@ -198,6 +200,7 @@
android:background="@drawable/bg_circle_white_60" android:background="@drawable/bg_circle_white_60"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_crosshair_24" android:src="@drawable/ic_crosshair_24"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintEnd_toStartOf="@id/topActionClock"
app:layout_constraintTop_toTopOf="@id/topActionMore" /> app:layout_constraintTop_toTopOf="@id/topActionMore" />
@ -439,7 +442,7 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_like_24" android:src="@drawable/ic_like_filled_24"
android:tint="#FF4081" /> android:tint="#FF4081" />
</FrameLayout> </FrameLayout>
@ -453,7 +456,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="我的收藏" android:text="我的点赞"
android:textColor="#111111" android:textColor="#111111"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" /> android:textStyle="bold" />
@ -488,8 +491,8 @@
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_heart_24" android:src="@drawable/ic_star_24"
android:tint="#E91E63" /> android:tint="#FFA726" />
</FrameLayout> </FrameLayout>
@ -502,7 +505,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="我的挚友" android:text="我的收藏"
android:textColor="#111111" android:textColor="#111111"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" /> android:textStyle="bold" />
@ -512,7 +515,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:text="0" android:text="0"
android:textColor="#999999" android:textColor="#999999"
android:textSize="11sp" /> android:textSize="11sp" />
@ -525,7 +528,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="64dp" android:layout_height="64dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:visibility="gone">
<FrameLayout <FrameLayout
android:layout_width="44dp" android:layout_width="44dp"
@ -686,6 +690,91 @@
</LinearLayout> </LinearLayout>
<!-- 我的作品区域 -->
<LinearLayout
android:id="@+id/myWorksSection"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/walletButton">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="我的作品"
android:textColor="#111111"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/myWorksCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0个作品"
android:textColor="#999999"
android:textSize="12sp" />
</LinearLayout>
<!-- 作品列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myWorksRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
<!-- 空状态 -->
<LinearLayout
android:id="@+id/myWorksEmptyState"
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:layout_width="56dp"
android:layout_height="56dp"
android:alpha="0.5"
android:src="@drawable/ic_add_photo_24"
android:tint="#999999" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="还没有发布作品"
android:textColor="#999999"
android:textSize="14sp" />
<TextView
android:id="@+id/myWorksPublishBtn"
android:layout_width="100dp"
android:layout_height="36dp"
android:layout_marginTop="12dp"
android:background="@drawable/bg_purple_20"
android:gravity="center"
android:text="去发布"
android:textColor="#FFFFFF"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/profileTabs" android:id="@+id/profileTabs"
android:layout_width="0dp" android:layout_width="0dp"
@ -700,7 +789,7 @@
app:tabTextColor="#666666" app:tabTextColor="#666666"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottomButtons"> app:layout_constraintTop_toBottomOf="@id/myWorksSection">
<com.google.android.material.tabs.TabItem <com.google.android.material.tabs.TabItem
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -348,6 +348,58 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- 分类选择卡片 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="作品分类"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<!-- 分类选择下拉框 -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/categoryInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请选择分类"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
app:boxCornerRadiusTopStart="8dp"
app:boxCornerRadiusTopEnd="8dp"
app:boxCornerRadiusBottomStart="8dp"
app:boxCornerRadiusBottomEnd="8dp"
app:hintTextColor="#999999">
<AutoCompleteTextView
android:id="@+id/categorySpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- 高级设置卡片 --> <!-- 高级设置卡片 -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -150,32 +150,32 @@
android:src="@drawable/ic_arrow_back_24" android:src="@drawable/ic_arrow_back_24"
app:tint="@android:color/white" /> app:tint="@android:color/white" />
<!-- 主播信息卡片 - 放在顶部栏内 --> <!-- 主播信息卡片 - TRTC风格 -->
<LinearLayout <LinearLayout
android:id="@+id/roomInfoLayout" android:id="@+id/roomInfoLayout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@drawable/bg_rounded_semi_transparent" android:background="@drawable/bg_trtc_info_card"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="8dp" android:paddingHorizontal="10dp"
android:paddingVertical="6dp"> android:paddingVertical="8dp">
<de.hdodenhof.circleimageview.CircleImageView <de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/streamerAvatar" android:id="@+id/streamerAvatar"
android:layout_width="32dp" android:layout_width="36dp"
android:layout_height="32dp" android:layout_height="36dp"
android:src="@drawable/ic_person_24" android:src="@drawable/ic_person_24"
app:civ_border_color="#FFFFFF" app:civ_border_color="@color/trtc_accent"
app:civ_border_width="1dp" /> app:civ_border_width="2dp" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="8dp" android:layout_marginStart="10dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -183,8 +183,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="主播" android:text="主播"
android:textColor="@android:color/white" android:textColor="@color/trtc_text_primary"
android:textSize="13sp" android:textSize="14sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
@ -195,68 +195,70 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:text="直播间" android:text="直播间"
android:textColor="#CCFFFFFF" android:textColor="@color/trtc_text_secondary"
android:textSize="11sp" /> android:textSize="12sp" />
</LinearLayout> </LinearLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/followButton" android:id="@+id/followButton"
style="@style/Widget.Material3.Button.TonalButton" style="@style/Widget.Material3.Button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="26dp" android:layout_height="32dp"
android:layout_marginStart="4dp" android:layout_marginStart="6dp"
android:minWidth="48dp" android:minWidth="56dp"
android:paddingHorizontal="10dp" android:paddingHorizontal="12dp"
android:text="关注" android:text="关注"
android:textColor="@android:color/white" android:textColor="@color/trtc_text_primary"
android:textSize="11sp" android:textSize="12sp"
android:textAllCaps="false" android:textAllCaps="false"
app:backgroundTint="#FF4081" app:backgroundTint="@color/trtc_accent"
app:cornerRadius="13dp" /> app:cornerRadius="16dp" />
</LinearLayout> </LinearLayout>
<!-- 直播状态标签 --> <!-- 直播状态标签 - TRTC风格 -->
<TextView <TextView
android:id="@+id/liveTag" android:id="@+id/liveTag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@drawable/live_badge_background" android:background="@drawable/bg_trtc_button_primary"
android:paddingHorizontal="8dp" android:paddingHorizontal="10dp"
android:paddingVertical="3dp" android:paddingVertical="4dp"
android:text="● 直播中" android:text="● LIVE"
android:textColor="@android:color/white" android:textColor="@color/trtc_text_primary"
android:textSize="11sp" android:textSize="12sp"
android:textStyle="bold"
android:visibility="visible" /> android:visibility="visible" />
<!-- 观看人数 --> <!-- 观看人数 - TRTC风格 -->
<LinearLayout <LinearLayout
android:id="@+id/topViewerLayout" android:id="@+id/topViewerLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@drawable/bg_rounded_semi_transparent" android:background="@drawable/bg_trtc_button_secondary"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="8dp" android:paddingHorizontal="10dp"
android:paddingVertical="4dp"> android:paddingVertical="6dp">
<ImageView <ImageView
android:layout_width="14dp" android:layout_width="16dp"
android:layout_height="14dp" android:layout_height="16dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:src="@drawable/ic_people_24" android:src="@drawable/ic_people_24"
app:tint="@android:color/white" /> app:tint="@color/trtc_text_primary" />
<TextView <TextView
android:id="@+id/topViewerCount" android:id="@+id/topViewerCount"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="0" android:text="0"
android:textColor="@android:color/white" android:textColor="@color/trtc_text_primary"
android:textSize="12sp" /> android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout> </LinearLayout>
@ -296,7 +298,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<!-- 右侧悬浮功能按钮 --> <!-- 右侧悬浮功能按钮 - TRTC风格 -->
<LinearLayout <LinearLayout
android:id="@+id/rightButtons" android:id="@+id/rightButtons"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -308,60 +310,61 @@
app:layout_constraintBottom_toTopOf="@id/chatInputLayout" app:layout_constraintBottom_toTopOf="@id/chatInputLayout"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<!-- 点赞按钮 --> <!-- 点赞按钮 - TRTC风格 -->
<FrameLayout <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="12dp"> android:layout_marginBottom="16dp">
<ImageButton <ImageButton
android:id="@+id/likeButton" android:id="@+id/likeButton"
android:layout_width="44dp" android:layout_width="48dp"
android:layout_height="44dp" android:layout_height="48dp"
android:background="@drawable/bg_circle_semi_transparent" android:background="@drawable/bg_trtc_button_secondary"
android:contentDescription="点赞" android:contentDescription="点赞"
android:src="@drawable/ic_like_24" android:src="@drawable/ic_like_24"
android:tint="#FF4081" /> android:tint="@color/trtc_accent" />
<TextView <TextView
android:id="@+id/likeCountText" android:id="@+id/likeCountText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal" android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="-2dp" android:layout_marginBottom="-4dp"
android:background="@drawable/bg_purple_20" android:background="@drawable/bg_trtc_button_primary"
android:paddingHorizontal="5dp" android:paddingHorizontal="6dp"
android:paddingVertical="1dp" android:paddingVertical="2dp"
android:text="0" android:text="0"
android:textColor="#FFFFFF" android:textColor="@color/trtc_text_primary"
android:textSize="9sp" /> android:textSize="10sp"
android:textStyle="bold" />
</FrameLayout> </FrameLayout>
<!-- 礼物按钮 --> <!-- 礼物按钮 - TRTC风格 -->
<ImageButton <ImageButton
android:id="@+id/giftButton" android:id="@+id/giftButton"
android:layout_width="44dp" android:layout_width="48dp"
android:layout_height="44dp" android:layout_height="48dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="16dp"
android:background="@drawable/bg_circle_semi_transparent" android:background="@drawable/bg_trtc_button_secondary"
android:contentDescription="送礼物" android:contentDescription="送礼物"
android:src="@drawable/ic_gift_24" android:src="@drawable/ic_gift_24"
android:tint="#FF9800" /> android:tint="@color/trtc_secondary" />
<!-- 分享按钮 --> <!-- 分享按钮 - TRTC风格 -->
<ImageButton <ImageButton
android:id="@+id/shareButton" android:id="@+id/shareButton"
android:layout_width="44dp" android:layout_width="48dp"
android:layout_height="44dp" android:layout_height="48dp"
android:background="@drawable/bg_circle_semi_transparent" android:background="@drawable/bg_trtc_button_secondary"
android:contentDescription="分享" android:contentDescription="分享"
android:src="@drawable/ic_share_24" android:src="@drawable/ic_share_24"
android:tint="@android:color/white" /> android:tint="@color/trtc_text_primary" />
</LinearLayout> </LinearLayout>
<!-- 底部输入栏 - 悬浮 --> <!-- 底部输入栏 - TRTC风格 -->
<LinearLayout <LinearLayout
android:id="@+id/chatInputLayout" android:id="@+id/chatInputLayout"
android:layout_width="0dp" android:layout_width="0dp"
@ -369,7 +372,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="12dp" android:paddingHorizontal="12dp"
android:paddingVertical="10dp" android:paddingVertical="12dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -378,27 +381,29 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/chatInput" android:id="@+id/chatInput"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="38dp" android:layout_height="42dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_chat_input_dark" android:background="@drawable/bg_trtc_input"
android:hint="说点什么..." android:hint="说点什么..."
android:textColorHint="#80FFFFFF" android:textColorHint="@color/trtc_text_hint"
android:imeOptions="actionSend" android:imeOptions="actionSend"
android:inputType="text" android:inputType="text"
android:maxLines="1" android:maxLines="1"
android:paddingHorizontal="14dp" android:paddingHorizontal="16dp"
android:textColor="@android:color/white" android:textColor="@color/trtc_text_primary"
android:textSize="13sp" /> android:textSize="14sp" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="38dp" android:layout_height="42dp"
android:layout_marginStart="8dp" android:layout_marginStart="10dp"
android:minWidth="64dp"
android:text="发送" android:text="发送"
android:textSize="12sp" android:textSize="13sp"
app:backgroundTint="#FF4081" android:textStyle="bold"
app:cornerRadius="19dp" /> app:backgroundTint="@color/trtc_accent"
app:cornerRadius="21dp" />
</LinearLayout> </LinearLayout>

View File

@ -114,6 +114,18 @@
android:paddingBottom="24dp" android:paddingBottom="24dp"
android:visibility="gone" /> android:visibility="gone" />
<!-- 作品列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/worksRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp"
android:paddingBottom="24dp"
android:visibility="gone" />
<!-- 旧的结果列表(兼容) --> <!-- 旧的结果列表(兼容) -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/resultsRecyclerView" android:id="@+id/resultsRecyclerView"

View File

@ -252,7 +252,7 @@
android:id="@+id/space_bio_margin_top" android:id="@+id/space_bio_margin_top"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHeight_percent="0.025" app:layout_constraintHeight_percent="0.015"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/avatarRing" /> app:layout_constraintTop_toBottomOf="@id/avatarRing" />
@ -271,7 +271,7 @@
android:id="@+id/space_addFriend_margin_top" android:id="@+id/space_addFriend_margin_top"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHeight_percent="0.028" app:layout_constraintHeight_percent="0.015"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bio" /> app:layout_constraintTop_toBottomOf="@id/bio" />
@ -292,10 +292,10 @@
android:layout_height="44dp" android:layout_height="44dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginEnd="6dp" android:layout_marginEnd="6dp"
android:background="@drawable/bg_follow_button" android:background="@drawable/bg_follow_button_normal"
android:gravity="center" android:gravity="center"
android:text="关注" android:text="关注"
android:textColor="#FF4757" android:textColor="#333333"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" /> android:textStyle="bold" />

View File

@ -80,7 +80,7 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<!-- 右侧操作按钮(点赞、收藏、评论) --> <!-- 右侧操作按钮(用户头像、关注、点赞、收藏、评论) -->
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -89,6 +89,40 @@
android:orientation="vertical" android:orientation="vertical"
android:gravity="center"> android:gravity="center">
<!-- 用户头像和关注按钮 -->
<FrameLayout
android:id="@+id/authorAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<!-- 用户头像 -->
<ImageView
android:id="@+id/authorAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/bg_avatar_circle"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_account_circle_24"
android:clickable="true"
android:focusable="true" />
<!-- 关注按钮(红色+号) -->
<ImageView
android:id="@+id/followButton"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="-6dp"
android:src="@drawable/ic_add_circle_24"
android:background="@drawable/bg_follow_button"
android:padding="2dp"
android:clickable="true"
android:focusable="true" />
</FrameLayout>
<!-- 点赞按钮 --> <!-- 点赞按钮 -->
<LinearLayout <LinearLayout
android:id="@+id/likeButtonContainer" android:id="@+id/likeButtonContainer"

View File

@ -1,110 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:paddingBottom="24dp">
<!-- 顶部标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="我的频道"
android:textSize="16sp"
android:textColor="#333333"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击进入频道"
android:textSize="12sp"
android:textColor="#999999"
android:layout_marginEnd="16dp" />
<TextView
android:id="@+id/btnEditChannels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="进入编辑"
android:textSize="13sp"
android:textColor="#333333"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:background="@drawable/bg_channel_edit_button" />
<ImageView
android:id="@+id/btnCloseChannelManager"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_expand_more_24"
android:rotation="180"
android:contentDescription="收起"
app:tint="#999999" />
</LinearLayout>
<!-- 我的频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myChannelsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingBottom="16dp"
android:clipToPadding="false" />
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:background="#F0F0F0" />
<!-- 推荐频道标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="推荐频道"
android:textSize="16sp"
android:textColor="#333333"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击添加频道"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
<!-- 推荐频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recommendChannelsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:clipToPadding="false" />
</LinearLayout>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@android:color/white">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="频道管理"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:gravity="center"
android:paddingBottom="16dp" />
<!-- 我的频道 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="我的频道"
android:textSize="14sp"
android:textColor="#666666" />
<TextView
android:id="@+id/addChannelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击进入频道,长按拖拽调整顺序"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
<!-- 我的频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/myChannelsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingHorizontal="8dp" />
<!-- 推荐频道 -->
<TextView
android:id="@+id/recommendTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="推荐频道"
android:textSize="14sp"
android:textColor="#666666"
android:paddingBottom="8dp" />
<!-- 推荐频道列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recommendChannelsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingHorizontal="8dp" />
<!-- 完成按钮 -->
<TextView
android:id="@+id/completeButton"
android:layout_width="match_parent"
android:layout_height="44dp"
android:text="完成"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="@drawable/bg_button_primary"
android:gravity="center"
android:layout_marginTop="8dp" />
</LinearLayout>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 空状态视图 -->
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:id="@+id/emptyIcon"
android:layout_width="80dp"
android:layout_height="80dp"
android:alpha="0.3"
android:src="@drawable/ic_grid_24" />
<TextView
android:id="@+id/emptyText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无内容"
android:textColor="#999999"
android:textSize="14sp" />
</LinearLayout>
<!-- 加载视图 -->
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -3,52 +3,31 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="4dp"> android:layout_margin="6dp">
<TextView <TextView
android:id="@+id/channelName" android:id="@+id/channelName"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingVertical="8dp" android:paddingVertical="10dp"
android:text="频道名" android:text="频道名"
android:textSize="14sp" android:textSize="13sp"
android:textColor="#333333" android:textColor="#666666"
android:background="@drawable/bg_channel_tag_normal" /> android:background="@drawable/bg_channel_tag_normal"
android:minWidth="60dp"
android:gravity="center" />
<!-- 固定标识前4个频道显示 --> <!-- 删除按钮(我的频道编辑模式显示) -->
<ImageView <ImageView
android:id="@+id/fixedIcon" android:id="@+id/deleteIcon"
android:layout_width="14dp" android:layout_width="18dp"
android:layout_height="14dp" android:layout_height="18dp"
android:layout_gravity="top|end" android:layout_gravity="top|end"
android:layout_marginTop="-4dp" android:layout_marginTop="-4dp"
android:layout_marginEnd="-4dp" android:layout_marginEnd="-4dp"
android:src="@drawable/ic_lock_12"
android:visibility="gone"
app:tint="#999999" />
<!-- 删除按钮(编辑模式显示) -->
<ImageView
android:id="@+id/deleteIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="top|end"
android:layout_marginTop="-6dp"
android:layout_marginEnd="-6dp"
android:src="@drawable/ic_close_circle_16" android:src="@drawable/ic_close_circle_16"
android:background="@drawable/bg_circle_white"
android:visibility="gone" /> android:visibility="gone" />
<!-- 添加按钮(推荐频道显示) -->
<ImageView
android:id="@+id/addIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="top|start"
android:layout_marginTop="6dp"
android:layout_marginStart="4dp"
android:src="@drawable/ic_add_12"
android:visibility="gone"
app:tint="#999999" />
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TRTC风格颜色主题 -->
<color name="trtc_primary">#FF6B35</color>
<color name="trtc_primary_dark">#E55A2B</color>
<color name="trtc_secondary">#4ECDC4</color>
<color name="trtc_accent">#FF4081</color>
<color name="trtc_accent_light">#FF6FA3</color>
<!-- 背景色 -->
<color name="trtc_background">#1A1A1A</color>
<color name="trtc_surface">#2D2D2D</color>
<color name="trtc_surface_light">#3D3D3D</color>
<!-- 文字颜色 -->
<color name="trtc_text_primary">#FFFFFF</color>
<color name="trtc_text_secondary">#CCFFFFFF</color>
<color name="trtc_text_hint">#80FFFFFF</color>
<!-- 半透明背景 -->
<color name="trtc_overlay_dark">#80000000</color>
<color name="trtc_overlay_light">#40FFFFFF</color>
<color name="trtc_overlay_accent">#40FF4081</color>
<!-- 状态颜色 -->
<color name="trtc_live_indicator">#FF4444</color>
<color name="trtc_online_indicator">#4CAF50</color>
<color name="trtc_offline_indicator">#999999</color>
</resources>

View File

@ -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. **分类管理测试**
- 点击首页右上角下拉按钮
- 验证分类管理对话框显示
- 测试添加/移除频道功能
## 五、完成状态
✅ 搜索功能修复完成
✅ 后端分类统一完成
✅ 下拉按钮点击事件添加完成
✅ 分类管理对话框创建完成
**可以重新编译测试了!**