功能:作品点赞+关注+收藏+作品搜索+作品标签

This commit is contained in:
xiao12feng8 2026-01-07 17:36:08 +08:00
parent d2fa9a1055
commit c8e22d497e
42 changed files with 2797 additions and 719 deletions

View File

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

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

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

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

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

@ -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,7 +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.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;
@ -14,7 +13,7 @@ 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.UserService; import com.zbkj.service.service.UserService;
import com.zbkj.service.service.WorksLikeService; import com.zbkj.service.service.WorksLikeService;
import com.zbkj.service.service.WorksCollectService; import com.zbkj.service.service.WorksCollectService;
@ -40,7 +39,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
private UserService userService; private UserService userService;
@Autowired @Autowired
private CategoryService categoryService; private LiveRoomCategoryService liveRoomCategoryService;
@Autowired @Autowired
private WorksLikeService worksLikeService; private WorksLikeService worksLikeService;
@ -77,8 +76,8 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
// 验证分类是否存在如果提供了分类ID // 验证分类是否存在如果提供了分类ID
if (request.getCategoryId() != null) { if (request.getCategoryId() != null) {
Category category = categoryService.getById(request.getCategoryId()); com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId());
if (category == null || !category.getStatus()) { if (category == null || category.getStatus() == null || category.getStatus() != 1) {
throw new CrmebException("分类不存在或已禁用"); throw new CrmebException("分类不存在或已禁用");
} }
} }
@ -166,8 +165,8 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
// 验证分类是否存在如果提供了分类ID // 验证分类是否存在如果提供了分类ID
if (request.getCategoryId() != null) { if (request.getCategoryId() != null) {
Category category = categoryService.getById(request.getCategoryId()); com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId());
if (category == null || !category.getStatus()) { if (category == null || category.getStatus() == null || category.getStatus() != 1) {
throw new CrmebException("分类不存在或已禁用"); throw new CrmebException("分类不存在或已禁用");
} }
} }
@ -421,7 +420,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
// 获取分类信息 // 获取分类信息
if (works.getCategoryId() != null) { if (works.getCategoryId() != null) {
Category category = categoryService.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,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,40 +71,72 @@ 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) {
if (isRecommendMode) { return;
tagText.setText("+ " + tag.getName());
} else {
tagText.setText(tag.getName());
} }
// 选中状态 try {
boolean isSelected = position == selectedPosition; // 显示文本
if (isSelected) { if (isRecommendMode) {
tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected); tagText.setText("+ " + tag.getName());
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.black, null)); deleteIcon.setVisibility(View.GONE);
} else { } else {
tagText.setBackgroundResource(R.drawable.bg_channel_tag); tagText.setText(tag.getName());
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null)); // 我的频道模式下除了"推荐"外都可以删除
} if (position == 0 && "推荐".equals(tag.getName())) {
deleteIcon.setVisibility(View.GONE);
// 点击事件
itemView.setOnClickListener(v -> {
if (listener != null) {
if (isRecommendMode) {
listener.onTagAddClick(tag, position);
} else { } else {
listener.onTagClick(tag, position); deleteIcon.setVisibility(View.VISIBLE);
} }
} }
});
// 选中状态样式 - 更明显的标签样式
boolean isSelected = position == selectedPosition;
if (isSelected) {
// 选中状态蓝色背景白色文字
tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected);
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.white, null));
} else {
if (isRecommendMode) {
// 推荐频道虚线边框紫色文字
tagText.setBackgroundResource(R.drawable.bg_channel_tag_recommend);
tagText.setTextColor(itemView.getContext().getResources().getColor(R.color.purple_500, null));
} else {
// 我的频道实线边框深灰色文字
tagText.setBackgroundResource(R.drawable.bg_channel_tag_normal);
tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null));
}
}
// 标签点击事件
tagText.setOnClickListener(v -> {
if (listener != null) {
if (isRecommendMode) {
listener.onTagAddClick(tag, position);
} else {
listener.onTagClick(tag, position);
}
}
});
// 删除按钮点击事件
deleteIcon.setOnClickListener(v -> {
if (listener != null && !isRecommendMode) {
listener.onTagClick(tag, position);
}
});
} catch (Exception e) {
android.util.Log.e("ChannelTagAdapter", "绑定标签数据失败", e);
}
} }
} }

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));
@ -398,6 +401,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);
@ -1751,6 +1765,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>>>() {
@ -1767,8 +1784,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() 未获取到分类数据,使用默认分类");
// 使用默认分类 // 使用默认分类
@ -1823,32 +1843,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() + " 个标签");
}); });
@ -3480,246 +3482,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;
}
}
}
/** /**
* 打开应用设置页面引导用户手动开启权限 * 打开应用设置页面引导用户手动开启权限
*/ */
@ -3741,4 +3503,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,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 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()));
}
});
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,400 @@
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()));
}
});
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,45 +563,91 @@ 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()); WorkDetailActivity.start(this, String.valueOf(work.getId()));
} }
}); });
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;
@ -112,6 +116,7 @@ public class PublishWorkActivity extends AppCompatActivity {
setupMediaAdapter(); setupMediaAdapter();
setupLaunchers(); setupLaunchers();
setupClickListeners(); setupClickListeners();
loadCategories(); // 加载分类数据
} }
private void setupToolbar() { private void setupToolbar() {
@ -347,6 +352,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();
@ -964,6 +972,11 @@ public class PublishWorkActivity extends AppCompatActivity {
commentSettingValue = "DISABLED"; commentSettingValue = "DISABLED";
} }
request.setCommentSetting(commentSettingValue); request.setCommentSetting(commentSettingValue);
// 设置分类ID
if (selectedCategory != null) {
request.setCategoryId(selectedCategory.getId());
}
ApiService apiService = ApiClient.getService(this); ApiService apiService = ApiClient.getService(this);
Call<ApiResponse<Long>> call = apiService.publishWork(request); Call<ApiResponse<Long>> call = apiService.publishWork(request);
@ -1488,5 +1501,101 @@ 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

@ -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;
@ -47,10 +49,14 @@ 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";
@ -789,6 +791,182 @@ public class WorkDetailActivity extends AppCompatActivity {
} }
} }
/**
* 设置作者头像和关注按钮
*/
private void setupAuthorSection() {
if (workItem == null) return;
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() { private void setupActionButton() {
// ============================================ // ============================================
// TODO: 判断是否是当前用户的作品 // TODO: 判断是否是当前用户的作品
@ -1051,6 +1229,7 @@ public class WorkDetailActivity extends AppCompatActivity {
setupContent(); setupContent();
setupActionButtons(); setupActionButtons();
setupActionButton(); setupActionButton();
setupAuthorSection(); // 设置作者头像和关注按钮
// 获取真实的评论数量 // 获取真实的评论数量
loadRealCommentCount(worksResponse.getId()); loadRealCommentCount(worksResponse.getId());
@ -1099,6 +1278,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);

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

@ -377,6 +377,23 @@ 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);
/**
* 获取当前用户发布的作品列表
*/
@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);
@DELETE("api/front/works/delete/{id}") @DELETE("api/front/works/delete/{id}")
Call<ApiResponse<Boolean>> deleteWork(@Path("id") long id); Call<ApiResponse<Boolean>> deleteWork(@Path("id") long id);
@ -979,4 +996,28 @@ public interface ApiService {
@Query("categoryId") Integer categoryId, @Query("categoryId") Integer categoryId,
@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/works/my/liked")
Call<ApiResponse<PageResponse<WorksResponse>>> getMyLikedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
/**
* 获取我收藏的作品列表
*/
@GET("api/front/works/my/collected")
Call<ApiResponse<PageResponse<WorksResponse>>> getMyCollectedWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
} }

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

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