diff --git a/Log/1-AI指南.md b/Log/1-AI指南.md index 4f1e7c89..a7a1bfaf 100644 --- a/Log/1-AI指南.md +++ b/Log/1-AI指南.md @@ -1,3 +1,8 @@ +# 手动引用 +1. 你明白我的意思吗。有没有不清楚的问题,请先询问我然后进行开发。有歧义要先询问我之后再进行下一步,不能你自己猜测可能的结果 +2. + + # AI工作指南 ## 🚀 快速引用 diff --git a/Log/8-配置上传服务器.md b/Log/8-配置上传服务器.md new file mode 100644 index 00000000..584f0cf5 --- /dev/null +++ b/Log/8-配置上传服务器.md @@ -0,0 +1,59 @@ +安装软件 +# 安装 Node.js 18 +curl -fsSL https://rpm.nodesource.com/setup_18.x | bash - +yum install -y nodejs + +# 验证 +node -v +npm -v + +npm install -g pm2 +pm2 start server.js --name upload-server +pm2 save +pm2 startup + +更改配置 +[root@VM-0-16-opencloudos ~]# # 重写正确的配置 +cat > /www/server/panel/vhost/nginx/1.15.149.240_30005.conf << 'EOF' +server +{ + listen 30005; + listen [::]:30005; + server_name 1.15.149.240_30005; + index index.php index.html index.htm default.php default.htm default.html; + root /www/wwwroot/1.15.149.240_30005; + + #CERT-APPLY-CHECK--START + include /www/server/panel/vhost/nginx/well-known/1.15.149.240_30005.conf; + #CERT-APPLY-CHECK--END + + #ERROR-PAGE-START + error_page 404 /404.html; + #ERROR-PAGE-END + + #PHP-INFO-START + include enable-php-84.conf; + #PHP-INFO-END + + #REWRITE-START + include /www/server/panel/vhost/rewrite/1.15.149.240_30005.conf; + #REWRITE-END + + # 上传API代理 + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + client_max_body_size 500M; + } + + location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) + { + return 404; + } + +nginx -s reload/www/wwwlogs/1.15.149.240_30005.error.log;a|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) { +nginx: [warn] conflicting server name "1.15.149.240" on 0.0.0.0:20001, ignored +nginx: the configuration file /www/server/nginx/conf/nginx.conf syntax is ok +nginx: configuration file /www/server/nginx/conf/nginx.conf test is successful +nginx: [warn] conflicting server name "1.15.149.240" on 0.0.0.0:20001, ignored \ No newline at end of file diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml b/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml index f7829dae..7f6a7d48 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/resources/application.yml @@ -36,7 +36,7 @@ LIVE_PUBLIC_SRS_HTTP_PORT: 25003 file: upload: server: - url: http://1.15.149.240:30005/upload # 文件上传服务器地址 + url: http://1.15.149.240:30005/api/upload # 文件上传服务器地址 # 配置端口 server: diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java index 87591c4f..6f8eb1a7 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/CategoryController.java @@ -67,14 +67,11 @@ public class CategoryController { @ApiOperation(value = "获取作品分类列表") @GetMapping("/work") public CommonResult> getWorkCategories() { - List categories = categoryService.getList( - new com.zbkj.common.request.CategorySearchRequest() - .setType(CategoryConstants.CATEGORY_TYPE_WORK) - .setStatus(CategoryConstants.CATEGORY_STATUS_NORMAL) - ); + // 暂时使用直播间分类作为作品分类,实现统一分类系统 + List liveCategories = liveRoomCategoryService.getEnabledList(); - List response = categories.stream() - .map(this::toCategoryResponse) + List response = liveCategories.stream() + .map(this::toLiveRoomCategoryResponse) .collect(Collectors.toList()); return CommonResult.success(response); diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml index 8b451731..9032c786 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml +++ b/Zhibo/zhibo-h/crmeb-front/src/main/resources/application.yml @@ -36,6 +36,12 @@ LIVE_PUBLIC_SRS_HOST: 1.15.149.240 LIVE_PUBLIC_SRS_RTMP_PORT: 25002 LIVE_PUBLIC_SRS_HTTP_PORT: 25003 +# ============ 文件上传服务器配置 ============ +file: + upload: + server: + url: http://1.15.149.240:30005/api/upload + spring: profiles: # 配置的环境 diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/RemoteUploadServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/RemoteUploadServiceImpl.java index 817c8052..0318b8da 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/RemoteUploadServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/RemoteUploadServiceImpl.java @@ -29,7 +29,7 @@ public class RemoteUploadServiceImpl { 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 RestTemplate restTemplate; diff --git a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/WorksServiceImpl.java b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/WorksServiceImpl.java index 5551a4fb..bd0e2c1b 100644 --- a/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/WorksServiceImpl.java +++ b/Zhibo/zhibo-h/crmeb-service/src/main/java/com/zbkj/service/service/impl/WorksServiceImpl.java @@ -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.service.impl.ServiceImpl; 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.works.Works; 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.response.WorksResponse; 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.WorksLikeService; import com.zbkj.service.service.WorksCollectService; @@ -40,7 +39,7 @@ public class WorksServiceImpl extends ServiceImpl implements Wo private UserService userService; @Autowired - private CategoryService categoryService; + private LiveRoomCategoryService liveRoomCategoryService; @Autowired private WorksLikeService worksLikeService; @@ -77,8 +76,8 @@ public class WorksServiceImpl extends ServiceImpl implements Wo // 验证分类是否存在(如果提供了分类ID) if (request.getCategoryId() != null) { - Category category = categoryService.getById(request.getCategoryId()); - if (category == null || !category.getStatus()) { + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); + if (category == null || category.getStatus() == null || category.getStatus() != 1) { throw new CrmebException("分类不存在或已禁用"); } } @@ -166,8 +165,8 @@ public class WorksServiceImpl extends ServiceImpl implements Wo // 验证分类是否存在(如果提供了分类ID) if (request.getCategoryId() != null) { - Category category = categoryService.getById(request.getCategoryId()); - if (category == null || !category.getStatus()) { + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(request.getCategoryId()); + if (category == null || category.getStatus() == null || category.getStatus() != 1) { throw new CrmebException("分类不存在或已禁用"); } } @@ -421,7 +420,7 @@ public class WorksServiceImpl extends ServiceImpl implements Wo // 获取分类信息 if (works.getCategoryId() != null) { - Category category = categoryService.getById(works.getCategoryId()); + com.zbkj.common.model.live.LiveRoomCategory category = liveRoomCategoryService.getById(works.getCategoryId()); if (category != null) { response.setCategoryName(category.getName()); } diff --git a/Zhibo/zhibo-h/sql/init_live_room_categories.sql b/Zhibo/zhibo-h/sql/init_live_room_categories.sql new file mode 100644 index 00000000..788753b2 --- /dev/null +++ b/Zhibo/zhibo-h/sql/init_live_room_categories.sql @@ -0,0 +1,18 @@ +-- 初始化直播间分类数据 +-- 确保eb_live_room_category表有基础的分类数据 + +USE zhibo; + +-- 插入基础的直播间分类数据(如果不存在) +INSERT IGNORE INTO eb_live_room_category (id, name, sort, status, create_time, update_time) VALUES +(1, '娱乐', 1, 1, NOW(), NOW()), +(2, '游戏', 2, 1, NOW(), NOW()), +(3, '音乐', 3, 1, NOW(), NOW()), +(4, '户外', 4, 1, NOW(), NOW()), +(5, '美食', 5, 1, NOW(), NOW()), +(6, '体育', 6, 1, NOW(), NOW()), +(7, '教育', 7, 1, NOW(), NOW()), +(8, '科技', 8, 1, NOW(), NOW()); + +-- 查看插入结果 +SELECT id, name, status FROM eb_live_room_category ORDER BY sort; \ No newline at end of file diff --git a/Zhibo/zhibo-h/sql/optimize_works_category.sql b/Zhibo/zhibo-h/sql/optimize_works_category.sql new file mode 100644 index 00000000..22690ef7 --- /dev/null +++ b/Zhibo/zhibo-h/sql/optimize_works_category.sql @@ -0,0 +1,12 @@ +-- 作品分类功能优化 +-- 作品表已经有 category_id 字段支持单分类选择 +-- 确保索引存在以提高查询性能 + +USE zhibo; + +-- 确保 category_id 字段存在并有正确的索引 +-- 如果索引不存在,创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_category_id ON eb_works(category_id); + +-- 更新表注释 +ALTER TABLE eb_works COMMENT = '作品表 - 支持单分类选择'; \ No newline at end of file diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 327a31e3..f5655ca1 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -101,6 +101,16 @@ android:name="com.example.livestreaming.LikedRoomsActivity" android:exported="false" /> + + + + diff --git a/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java deleted file mode 100644 index c35287f5..00000000 --- a/android-app/app/src/main/java/com/example/livestreaming/ChannelManagerAdapter.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.example.livestreaming; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -/** - * 频道管理适配器 - * 用于显示我的频道和推荐频道 - */ -public class ChannelManagerAdapter extends RecyclerView.Adapter { - - public interface OnChannelClickListener { - void onChannelClick(ChannelItem item, int position); - void onChannelDelete(ChannelItem item, int position); - void onChannelAdd(ChannelItem item, int position); - } - - private final List items = new ArrayList<>(); - private OnChannelClickListener listener; - private boolean isEditMode = false; - private boolean isRecommendMode = false; // 是否是推荐频道模式 - private int fixedCount = 4; // 前4个固定不能删除 - - public void setOnChannelClickListener(OnChannelClickListener listener) { - this.listener = listener; - } - - public void setEditMode(boolean editMode) { - this.isEditMode = editMode; - notifyDataSetChanged(); - } - - public void setRecommendMode(boolean recommendMode) { - this.isRecommendMode = recommendMode; - } - - public void setFixedCount(int count) { - this.fixedCount = count; - } - - public void submitList(List newItems) { - items.clear(); - if (newItems != null) { - items.addAll(newItems); - } - notifyDataSetChanged(); - } - - public List getItems() { - return new ArrayList<>(items); - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_channel_tag, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - ChannelItem item = items.get(position); - holder.bind(item, position); - } - - @Override - public int getItemCount() { - return items.size(); - } - - class ViewHolder extends RecyclerView.ViewHolder { - private final TextView channelName; - private final ImageView fixedIcon; - private final ImageView deleteIcon; - private final ImageView addIcon; - - ViewHolder(@NonNull View itemView) { - super(itemView); - channelName = itemView.findViewById(R.id.channelName); - fixedIcon = itemView.findViewById(R.id.fixedIcon); - deleteIcon = itemView.findViewById(R.id.deleteIcon); - addIcon = itemView.findViewById(R.id.addIcon); - } - - void bind(ChannelItem item, int position) { - channelName.setText(item.getName()); - - // 重置所有图标状态 - fixedIcon.setVisibility(View.GONE); - deleteIcon.setVisibility(View.GONE); - addIcon.setVisibility(View.GONE); - - if (isRecommendMode) { - // 推荐频道模式:显示添加图标 - addIcon.setVisibility(View.VISIBLE); - itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelAdd(item, position); - } - }); - } else { - // 我的频道模式 - boolean isFixed = position < fixedCount; - - if (isEditMode) { - // 编辑模式 - if (isFixed) { - fixedIcon.setVisibility(View.VISIBLE); - } else { - deleteIcon.setVisibility(View.VISIBLE); - deleteIcon.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelDelete(item, position); - } - }); - } - } - - itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onChannelClick(item, position); - } - }); - } - } - } - - /** - * 频道项数据类 - */ - public static class ChannelItem { - private final String id; - private final String name; - private boolean isFixed; - - public ChannelItem(String id, String name) { - this.id = id; - this.name = name; - this.isFixed = false; - } - - public ChannelItem(String id, String name, boolean isFixed) { - this.id = id; - this.name = name; - this.isFixed = isFixed; - } - - public String getId() { return id; } - public String getName() { return name; } - public boolean isFixed() { return isFixed; } - public void setFixed(boolean fixed) { this.isFixed = fixed; } - } -} diff --git a/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java index e4e81ea2..175265e2 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ChannelTagAdapter.java @@ -71,40 +71,72 @@ public class ChannelTagAdapter extends ListAdapter { - if (listener != null) { - if (isRecommendMode) { - listener.onTagAddClick(tag, position); + + try { + // 显示文本 + if (isRecommendMode) { + tagText.setText("+ " + tag.getName()); + deleteIcon.setVisibility(View.GONE); + } else { + tagText.setText(tag.getName()); + // 我的频道模式下,除了"推荐"外都可以删除 + if (position == 0 && "推荐".equals(tag.getName())) { + deleteIcon.setVisibility(View.GONE); } else { - listener.onTagClick(tag, position); + deleteIcon.setVisibility(View.VISIBLE); } } - }); + + // 选中状态样式 - 更明显的标签样式 + boolean isSelected = position == selectedPosition; + if (isSelected) { + // 选中状态:蓝色背景,白色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_selected); + tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.white, null)); + } else { + if (isRecommendMode) { + // 推荐频道:虚线边框,紫色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_recommend); + tagText.setTextColor(itemView.getContext().getResources().getColor(R.color.purple_500, null)); + } else { + // 我的频道:实线边框,深灰色文字 + tagText.setBackgroundResource(R.drawable.bg_channel_tag_normal); + tagText.setTextColor(itemView.getContext().getResources().getColor(android.R.color.darker_gray, null)); + } + } + + // 标签点击事件 + tagText.setOnClickListener(v -> { + if (listener != null) { + if (isRecommendMode) { + listener.onTagAddClick(tag, position); + } else { + listener.onTagClick(tag, position); + } + } + }); + + // 删除按钮点击事件 + deleteIcon.setOnClickListener(v -> { + if (listener != null && !isRecommendMode) { + listener.onTagClick(tag, position); + } + }); + } catch (Exception e) { + android.util.Log.e("ChannelTagAdapter", "绑定标签数据失败", e); + } } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 5a9debe9..2d67c44e 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -17,6 +17,7 @@ import android.speech.SpeechRecognizer; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.view.LayoutInflater; import android.view.View; import android.widget.ArrayAdapter; import android.widget.TextView; @@ -44,6 +45,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputLayout; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.ApiService; import com.example.livestreaming.net.CommunityResponse; import com.example.livestreaming.net.ConversationResponse; import com.example.livestreaming.net.CreateRoomRequest; @@ -201,7 +203,8 @@ public class MainActivity extends AppCompatActivity { // 隐藏粉丝和获赞菜单项 // items.add(new DrawerCardItem(DrawerCardItem.ACTION_FANS, "粉丝", "关注你的人", R.drawable.ic_people_24)); // items.add(new DrawerCardItem(DrawerCardItem.ACTION_LIKES, "获赞", "收到的点赞", R.drawable.ic_heart_24)); - items.add(new DrawerCardItem(DrawerCardItem.ACTION_HISTORY, "观看历史", "最近看过的直播", R.drawable.ic_grid_24)); + // 隐藏观看历史菜单项 + // items.add(new DrawerCardItem(DrawerCardItem.ACTION_HISTORY, "观看历史", "最近看过的直播", R.drawable.ic_grid_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_SEARCH, "搜索", "找主播/房间/标签", R.drawable.ic_search_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_SETTINGS, "设置", "账号、隐私、通知", R.drawable.ic_menu_24)); items.add(new DrawerCardItem(DrawerCardItem.ACTION_HELP, "帮助与反馈", "常见问题与建议", R.drawable.ic_chat_24)); @@ -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 { View notificationIcon = findViewById(R.id.notificationIcon); @@ -1751,6 +1765,9 @@ public class MainActivity extends AppCompatActivity { private void loadCategoriesFromBackend() { Log.d(TAG, "loadCategoriesFromBackend() 开始加载直播间分类"); + // 先从本地加载我的频道配置 + loadMyChannelsFromPrefs(); + // 调用后端接口获取直播间分类列表 ApiClient.getService(getApplicationContext()).getLiveRoomCategories() .enqueue(new Callback>>() { @@ -1767,8 +1784,11 @@ public class MainActivity extends AppCompatActivity { if (categories != null && !categories.isEmpty()) { Log.d(TAG, "loadCategoriesFromBackend() 成功获取 " + categories.size() + " 个分类"); - // 更新分类标签 - updateCategoryTabs(categories); + // 缓存后端分类数据 + allBackendCategories.clear(); + allBackendCategories.addAll(categories); + // 使用我的频道配置更新分类标签 + updateCategoryTabsFromMyChannels(); } else { Log.w(TAG, "loadCategoriesFromBackend() 未获取到分类数据,使用默认分类"); // 使用默认分类 @@ -1823,32 +1843,14 @@ public class MainActivity extends AppCompatActivity { private void useDefaultCategories() { if (binding == null || binding.categoryTabs == null) return; + // 如果我的频道配置为空,初始化默认配置 + if (myChannels.isEmpty()) { + initDefaultMyChannels(); + } + runOnUiThread(() -> { - // 清空现有标签 - binding.categoryTabs.removeAllTabs(); - - // 使用后端分类(如果已加载) - if (!allBackendCategories.isEmpty()) { - // 添加"推荐"作为第一个标签 - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐")); - - // 添加后端分类 - for (com.example.livestreaming.net.CategoryResponse cat : allBackendCategories) { - if (cat != null && cat.getName() != null) { - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(cat.getName())); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("推荐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("娱乐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("游戏")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("音乐")); - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText("户外")); - } - - // 恢复上次选中的分类 - restoreCategoryTabSelection(); + // 使用我的频道配置更新分类标签 + updateCategoryTabsFromMyChannels(); Log.d(TAG, "useDefaultCategories() 使用默认分类,共 " + binding.categoryTabs.getTabCount() + " 个标签"); }); @@ -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 myChannelList = loadMyChannels(); - myAdapter.submitList(myChannelList); - - // 加载推荐频道数据 - List recommendList = loadRecommendChannels(myChannelList); - recommendAdapter.submitList(recommendList); - - // 设置我的频道点击事件 - myAdapter.setOnChannelClickListener(new ChannelManagerAdapter.OnChannelClickListener() { - @Override - public void onChannelClick(ChannelManagerAdapter.ChannelItem item, int position) { - // 点击频道,切换到该分类 - dialog.dismiss(); - selectCategoryTab(item.getName()); - } - - @Override - public void onChannelDelete(ChannelManagerAdapter.ChannelItem item, int position) { - // 删除频道 - List currentList = myAdapter.getItems(); - currentList.remove(position); - myAdapter.submitList(new ArrayList<>(currentList)); - saveMyChannels(currentList); - // 更新推荐列表 - recommendAdapter.submitList(loadRecommendChannels(currentList)); - } - - @Override - public void onChannelAdd(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - }); - - // 设置推荐频道点击事件 - recommendAdapter.setOnChannelClickListener(new ChannelManagerAdapter.OnChannelClickListener() { - @Override - public void onChannelClick(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - - @Override - public void onChannelDelete(ChannelManagerAdapter.ChannelItem item, int position) { - // 不处理 - } - - @Override - public void onChannelAdd(ChannelManagerAdapter.ChannelItem item, int position) { - // 添加到我的频道 - List currentList = myAdapter.getItems(); - currentList.add(new ChannelManagerAdapter.ChannelItem(item.getId(), item.getName())); - myAdapter.submitList(new ArrayList<>(currentList)); - saveMyChannels(currentList); - // 更新推荐列表 - recommendAdapter.submitList(loadRecommendChannels(currentList)); - } - }); - - // 编辑按钮点击事件 - final boolean[] isEditMode = {false}; - if (btnEditChannel != null) { - btnEditChannel.setOnClickListener(v -> { - isEditMode[0] = !isEditMode[0]; - myAdapter.setEditMode(isEditMode[0]); - btnEditChannel.setText(isEditMode[0] ? "完成" : "编辑"); - }); - } - - // 关闭按钮点击事件 - if (btnCloseChannelManager != null) { - btnCloseChannelManager.setOnClickListener(v -> dialog.dismiss()); - } - - dialog.show(); - } - - /** - * 加载我的频道列表 - */ - private List loadMyChannels() { - List channels = new ArrayList<>(); - - // 从SharedPreferences加载 - android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); - - // 检查是否需要迁移到新版本(使用后端分类) - int savedVersion = prefs.getInt("channel_version", 0); - if (savedVersion < 2) { - // 旧版本数据,清除并使用后端分类 - prefs.edit() - .remove("my_channels") - .putInt("channel_version", 2) - .apply(); - Log.d(TAG, "loadMyChannels() 清除旧版本频道数据,使用后端分类"); - } - - String savedChannels = prefs.getString("my_channels", null); - - if (savedChannels != null && !savedChannels.isEmpty()) { - String[] channelNames = savedChannels.split(","); - for (int i = 0; i < channelNames.length; i++) { - channels.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(i), channelNames[i], i < 4)); - } - } else { - // 使用后端分类作为默认频道(如果有的话) - if (!allBackendCategories.isEmpty()) { - // 添加"推荐"作为第一个固定频道 - channels.add(new ChannelManagerAdapter.ChannelItem("0", "推荐", true)); - // 添加后端分类(最多取前4个作为默认) - int count = Math.min(allBackendCategories.size(), 4); - for (int i = 0; i < count; i++) { - com.example.livestreaming.net.CategoryResponse cat = allBackendCategories.get(i); - if (cat != null && cat.getName() != null) { - channels.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(cat.getId()), cat.getName(), true)); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - channels.add(new ChannelManagerAdapter.ChannelItem("0", "推荐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("1", "娱乐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("2", "游戏", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("3", "音乐", true)); - channels.add(new ChannelManagerAdapter.ChannelItem("4", "户外", true)); - } - } - - return channels; - } - - /** - * 保存我的频道列表 - */ - private void saveMyChannels(List channels) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < channels.size(); i++) { - if (i > 0) sb.append(","); - sb.append(channels.get(i).getName()); - } - - android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); - prefs.edit().putString("my_channels", sb.toString()).apply(); - - // 更新分类标签 - updateCategoryTabsFromChannels(channels); - } - - /** - * 加载推荐频道列表(排除已添加的) - */ - private List loadRecommendChannels(List myChannels) { - // 获取已添加的频道名称 - java.util.Set addedNames = new java.util.HashSet<>(); - for (ChannelManagerAdapter.ChannelItem item : myChannels) { - addedNames.add(item.getName()); - } - - List recommendList = new ArrayList<>(); - - // 优先使用后端分类 - if (!allBackendCategories.isEmpty()) { - for (com.example.livestreaming.net.CategoryResponse cat : allBackendCategories) { - if (cat != null && cat.getName() != null && !addedNames.contains(cat.getName())) { - recommendList.add(new ChannelManagerAdapter.ChannelItem( - String.valueOf(cat.getId()), cat.getName())); - } - } - } else { - // 后端分类未加载,使用硬编码默认值 - String[] defaultChannels = {"娱乐", "游戏", "音乐", "户外", "聊天"}; - for (int i = 0; i < defaultChannels.length; i++) { - if (!addedNames.contains(defaultChannels[i])) { - recommendList.add(new ChannelManagerAdapter.ChannelItem(String.valueOf(i), defaultChannels[i])); - } - } - } - - return recommendList; - } - - /** - * 根据频道列表更新分类标签 - */ - private void updateCategoryTabsFromChannels(List channels) { - if (binding.categoryTabs == null) return; - - binding.categoryTabs.removeAllTabs(); - for (ChannelManagerAdapter.ChannelItem channel : channels) { - binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName())); - } - } - - /** - * 选中指定分类标签 - */ - private void selectCategoryTab(String categoryName) { - if (binding.categoryTabs == null) return; - - for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) { - TabLayout.Tab tab = binding.categoryTabs.getTabAt(i); - if (tab != null && categoryName.equals(tab.getText())) { - tab.select(); - break; - } - } - } - /** * 打开应用设置页面,引导用户手动开启权限 */ @@ -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>> call = + apiService.getLiveRoomCategories(); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + try { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List categories = apiResponse.getData(); + + // 在主线程中更新UI + runOnUiThread(() -> { + try { + // 清空推荐频道 + recommendChannels.clear(); + + // 获取我的频道中已有的分类ID + Set myChannelIds = new HashSet<>(); + for (ChannelTagAdapter.ChannelTag myChannel : myChannels) { + if (myChannel != null) { + myChannelIds.add(myChannel.getId()); + } + } + + // 将不在我的频道中的分类添加到推荐频道 + for (com.example.livestreaming.net.CategoryResponse category : categories) { + if (category != null && category.getName() != null && + !myChannelIds.contains(category.getId())) { + recommendChannels.add(new ChannelTagAdapter.ChannelTag(category.getId(), category.getName())); + } + } + + // 更新适配器 + if (myChannelAdapter != null) { + myChannelAdapter.submitList(new ArrayList<>(myChannels)); + } + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + } catch (Exception e) { + Log.e(TAG, "更新推荐频道失败", e); + initDefaultRecommendChannels(); + } + }); + } else { + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } else { + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } catch (Exception e) { + Log.e(TAG, "处理分类数据失败", e); + runOnUiThread(() -> initDefaultRecommendChannels()); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载分类失败", t); + // 如果API调用失败,使用默认的推荐频道 + runOnUiThread(() -> initDefaultRecommendChannels()); + } + }); + } + + /** + * 初始化默认的推荐频道 + */ + private void initDefaultRecommendChannels() { + try { + recommendChannels.clear(); + + // 获取我的频道中已有的分类名称 + Set myChannelNames = new HashSet<>(); + for (ChannelTagAdapter.ChannelTag myChannel : myChannels) { + if (myChannel != null && myChannel.getName() != null) { + myChannelNames.add(myChannel.getName()); + } + } + + // 添加一些默认的推荐频道(不在我的频道中的) + String[] defaultChannels = {"游戏", "舞蹈", "二次元", "体育", "户外", "才艺", "美食"}; + int id = 100; // 使用较大的ID避免冲突 + for (String channelName : defaultChannels) { + if (channelName != null && !myChannelNames.contains(channelName)) { + recommendChannels.add(new ChannelTagAdapter.ChannelTag(id++, channelName)); + } + } + + // 更新适配器 + if (recommendChannelAdapter != null) { + recommendChannelAdapter.submitList(new ArrayList<>(recommendChannels)); + } + } catch (Exception e) { + Log.e(TAG, "初始化默认推荐频道失败", e); + } + } + + /** + * 从我的频道更新分类标签 + */ + private void updateCategoryTabsFromMyChannels() { + try { + if (binding == null || binding.categoryTabs == null) { + Log.w(TAG, "binding或categoryTabs为null,无法更新分类标签"); + return; + } + + binding.categoryTabs.removeAllTabs(); + + // 添加我的频道到分类标签 + for (ChannelTagAdapter.ChannelTag channel : myChannels) { + if (channel != null && channel.getName() != null) { + binding.categoryTabs.addTab(binding.categoryTabs.newTab().setText(channel.getName())); + } + } + + // 选中第一个标签 + if (myChannels.size() > 0 && binding.categoryTabs.getTabCount() > 0) { + ChannelTagAdapter.ChannelTag firstChannel = myChannels.get(0); + if (firstChannel != null && firstChannel.getName() != null) { + currentCategory = firstChannel.getName(); + TabLayout.Tab firstTab = binding.categoryTabs.getTabAt(0); + if (firstTab != null) { + firstTab.select(); + } + } + } + } catch (Exception e) { + Log.e(TAG, "更新分类标签失败", e); + } + } + + /** + * 保存我的频道配置到本地 + */ + private void saveMyChannelsToPrefs() { + android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); + android.content.SharedPreferences.Editor editor = prefs.edit(); + + // 将我的频道列表转换为JSON字符串保存 + StringBuilder channelsJson = new StringBuilder(); + channelsJson.append("["); + for (int i = 0; i < myChannels.size(); i++) { + ChannelTagAdapter.ChannelTag channel = myChannels.get(i); + if (i > 0) channelsJson.append(","); + channelsJson.append("{\"id\":").append(channel.getId()) + .append(",\"name\":\"").append(channel.getName()).append("\"}"); + } + channelsJson.append("]"); + + editor.putString("my_channels", channelsJson.toString()); + editor.apply(); + + Log.d(TAG, "保存我的频道配置: " + channelsJson.toString()); + } + + /** + * 从本地加载我的频道配置 + */ + private void loadMyChannelsFromPrefs() { + android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); + String channelsJson = prefs.getString("my_channels", ""); + + if (!channelsJson.isEmpty()) { + try { + // 简单解析JSON字符串 + myChannels.clear(); + channelsJson = channelsJson.trim(); + if (channelsJson.startsWith("[") && channelsJson.endsWith("]")) { + channelsJson = channelsJson.substring(1, channelsJson.length() - 1); + if (!channelsJson.isEmpty()) { + String[] items = channelsJson.split("\\},\\{"); + for (String item : items) { + item = item.replace("{", "").replace("}", ""); + String[] parts = item.split(","); + if (parts.length >= 2) { + int id = Integer.parseInt(parts[0].split(":")[1]); + String name = parts[1].split(":")[1].replace("\"", ""); + myChannels.add(new ChannelTagAdapter.ChannelTag(id, name)); + } + } + } + } + Log.d(TAG, "加载我的频道配置: " + myChannels.size() + " 个频道"); + } catch (Exception e) { + Log.e(TAG, "解析我的频道配置失败", e); + // 如果解析失败,使用默认配置 + initDefaultMyChannels(); + } + } else { + // 如果没有保存的配置,使用默认配置 + initDefaultMyChannels(); + } + } + + /** + * 初始化默认的我的频道配置 + */ + private void initDefaultMyChannels() { + myChannels.clear(); + myChannels.add(new ChannelTagAdapter.ChannelTag(0, "推荐")); + myChannels.add(new ChannelTagAdapter.ChannelTag(1, "直播")); + myChannels.add(new ChannelTagAdapter.ChannelTag(2, "视频")); + myChannels.add(new ChannelTagAdapter.ChannelTag(3, "音乐")); + Log.d(TAG, "使用默认我的频道配置"); + } +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java new file mode 100644 index 00000000..e857aecf --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/MyCollectionsActivity.java @@ -0,0 +1,401 @@ +package com.example.livestreaming; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.example.livestreaming.net.ApiClient; +import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; +import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.WorksResponse; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * 我的收藏页面 - 分类显示作品收藏和直播间收藏 + */ +public class MyCollectionsActivity extends AppCompatActivity { + + private TabLayout tabLayout; + private ViewPager2 viewPager; + + public static void start(Context context) { + Intent intent = new Intent(context, MyCollectionsActivity.class); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_my_collections); + + setupToolbar(); + setupViewPager(); + } + + private void setupToolbar() { + androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + toolbar.setNavigationOnClickListener(v -> finish()); + } + } + + private void setupViewPager() { + tabLayout = findViewById(R.id.tabLayout); + viewPager = findViewById(R.id.viewPager); + + CollectionsPagerAdapter adapter = new CollectionsPagerAdapter(this); + viewPager.setAdapter(adapter); + + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText("作品"); + break; + case 1: + tab.setText("直播间"); + break; + } + }).attach(); + } + + /** + * ViewPager适配器 + */ + private static class CollectionsPagerAdapter extends FragmentStateAdapter { + public CollectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new CollectedWorksFragment(); + } else { + return new CollectedRoomsFragment(); + } + } + + @Override + public int getItemCount() { + return 2; + } + } + + + /** + * 收藏的作品Fragment + */ + public static class CollectedWorksFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private WorksAdapter adapter; + private final List collectedWorks = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_star_24); + if (emptyText != null) emptyText.setText("还没有收藏过作品"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new WorksAdapter(work -> { + if (work != null && getActivity() != null) { + WorkDetailActivity.start(getActivity(), String.valueOf(work.getId())); + } + }); + recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 4) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + ApiClient.getService(getContext()) + .getMyCollectedWorks(currentPage, 20) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) collectedWorks.clear(); + collectedWorks.addAll(pageData.getList()); + adapter.submitList(new ArrayList<>(collectedWorks)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (collectedWorks.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } + + /** + * 收藏的直播间Fragment(暂时复用点赞的直播间数据,后续可扩展) + */ + public static class CollectedRoomsFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private RoomsAdapter adapter; + private final List collectedRooms = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_live_24); + if (emptyText != null) emptyText.setText("还没有收藏过直播间"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { + if (room != null && getActivity() != null) { + Intent intent = new Intent(getActivity(), RoomDetailActivity.class); + intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); + startActivity(intent); + } + }); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 2) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + // 暂时使用点赞的直播间接口,后续可以添加专门的收藏直播间接口 + ApiClient.getService(getContext()) + .getMyLikedRooms(currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) collectedRooms.clear(); + collectedRooms.addAll(convertToRooms(pageData.getList())); + adapter.submitList(new ArrayList<>(collectedRooms)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private List convertToRooms(List> roomMaps) { + List rooms = new ArrayList<>(); + for (Map map : roomMaps) { + Room room = new Room(); + if (map.containsKey("roomId")) room.setId(map.get("roomId")); + if (map.containsKey("roomTitle")) room.setTitle((String) map.get("roomTitle")); + if (map.containsKey("streamerName")) room.setStreamerName((String) map.get("streamerName")); + if (map.containsKey("streamerId")) room.setStreamerId(((Number) map.get("streamerId")).intValue()); + if (map.containsKey("coverImage")) room.setCoverImage((String) map.get("coverImage")); + if (map.containsKey("isLive")) { + Object isLive = map.get("isLive"); + if (isLive instanceof Number) room.setLive(((Number) isLive).intValue() == 1); + else if (isLive instanceof Boolean) room.setLive((Boolean) isLive); + } + rooms.add(room); + } + return rooms; + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (collectedRooms.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java new file mode 100644 index 00000000..8bc64733 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/MyLikesActivity.java @@ -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 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>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) likedWorks.clear(); + likedWorks.addAll(pageData.getList()); + adapter.submitList(new ArrayList<>(likedWorks)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (likedWorks.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } + + /** + * 点赞的直播间Fragment + */ + public static class LikedRoomsFragment extends Fragment { + private RecyclerView recyclerView; + private SwipeRefreshLayout swipeRefreshLayout; + private View emptyView; + private View loadingView; + private RoomsAdapter adapter; + private final List likedRooms = new ArrayList<>(); + private int currentPage = 1; + private boolean isLoading = false; + private boolean hasMore = true; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_list_content, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViews(view); + setupRecyclerView(); + loadData(); + } + + private void initViews(View view) { + recyclerView = view.findViewById(R.id.recyclerView); + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout); + emptyView = view.findViewById(R.id.emptyView); + loadingView = view.findViewById(R.id.loadingView); + + ImageView emptyIcon = view.findViewById(R.id.emptyIcon); + TextView emptyText = view.findViewById(R.id.emptyText); + if (emptyIcon != null) emptyIcon.setImageResource(R.drawable.ic_live_24); + if (emptyText != null) emptyText.setText("还没有点赞过直播间"); + + swipeRefreshLayout.setOnRefreshListener(() -> { + currentPage = 1; + hasMore = true; + loadData(); + }); + } + + private void setupRecyclerView() { + adapter = new RoomsAdapter(room -> { + if (room != null && getActivity() != null) { + Intent intent = new Intent(getActivity(), RoomDetailActivity.class); + intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); + startActivity(intent); + } + }); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 && !isLoading && hasMore) { + LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (lm != null) { + int total = lm.getItemCount(); + int lastVisible = lm.findLastVisibleItemPosition(); + if (lastVisible >= total - 2) { + currentPage++; + loadData(); + } + } + } + } + }); + } + + private void loadData() { + if (isLoading || getContext() == null) return; + isLoading = true; + if (currentPage == 1) showLoading(); + + ApiClient.getService(getContext()) + .getMyLikedRooms(currentPage, 20) + .enqueue(new Callback>>>() { + @Override + public void onResponse(Call>>> call, + Response>>> response) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse> pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null) { + if (currentPage == 1) likedRooms.clear(); + likedRooms.addAll(convertToRooms(pageData.getList())); + adapter.submitList(new ArrayList<>(likedRooms)); + hasMore = pageData.getList().size() >= 20; + } + } + updateEmptyState(); + } + + @Override + public void onFailure(Call>>> call, Throwable t) { + isLoading = false; + hideLoading(); + swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private List convertToRooms(List> roomMaps) { + List rooms = new ArrayList<>(); + for (Map map : roomMaps) { + Room room = new Room(); + if (map.containsKey("roomId")) room.setId(map.get("roomId")); + if (map.containsKey("roomTitle")) room.setTitle((String) map.get("roomTitle")); + if (map.containsKey("streamerName")) room.setStreamerName((String) map.get("streamerName")); + if (map.containsKey("streamerId")) room.setStreamerId(((Number) map.get("streamerId")).intValue()); + if (map.containsKey("coverImage")) room.setCoverImage((String) map.get("coverImage")); + if (map.containsKey("isLive")) { + Object isLive = map.get("isLive"); + if (isLive instanceof Number) room.setLive(((Number) isLive).intValue() == 1); + else if (isLive instanceof Boolean) room.setLive((Boolean) isLive); + } + rooms.add(room); + } + return rooms; + } + + private void showLoading() { + if (loadingView != null) loadingView.setVisibility(View.VISIBLE); + } + + private void hideLoading() { + if (loadingView != null) loadingView.setVisibility(View.GONE); + } + + private void updateEmptyState() { + if (likedRooms.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + } + } + } +} diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java index 06e75efd..5e0a46f5 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java @@ -30,7 +30,9 @@ import com.example.livestreaming.ShareUtils; import com.example.livestreaming.location.TianDiTuLocationService; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.ApiResponse; +import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.UserInfoResponse; +import com.example.livestreaming.net.WorksResponse; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -70,6 +72,7 @@ public class ProfileActivity extends AppCompatActivity { private ActivityResultLauncher editProfileLauncher; private UserWorksAdapter worksAdapter; + private WorksAdapter myWorksAdapter; public static void start(Context context) { Intent intent = new Intent(context, ProfileActivity.class); @@ -437,13 +440,19 @@ public class ProfileActivity extends AppCompatActivity { startActivity(new Intent(this, FollowingActivity.class)); }); binding.action2.setOnClickListener(v -> { - // 我的收藏(点赞的直播间) + // 我的点赞(作品+直播间) + if (!AuthHelper.requireLogin(this, "查看点赞需要登录")) { + return; + } + MyLikesActivity.start(this); + }); + binding.action3.setOnClickListener(v -> { + // 我的收藏(作品+直播间) if (!AuthHelper.requireLogin(this, "查看收藏需要登录")) { return; } - startActivity(new Intent(this, LikedRoomsActivity.class)); + MyCollectionsActivity.start(this); }); - binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class))); binding.action4.setOnClickListener(v -> { // 我的记录 - 跳转到统一记录页面 if (!AuthHelper.requireLogin(this, "查看记录需要登录")) { @@ -474,11 +483,11 @@ public class ProfileActivity extends AppCompatActivity { }); binding.addFriendBtn.setOnClickListener(v -> { - // 检查登录状态,添加好友需要登录 - if (!AuthHelper.requireLogin(this, "添加好友需要登录")) { + // 我的挚友(原添加好友功能已在挚友页面内) + if (!AuthHelper.requireLogin(this, "查看挚友需要登录")) { return; } - AddFriendActivity.start(this); + startActivity(new Intent(this, MyFriendsActivity.class)); }); // 我的钱包按钮点击事件 @@ -554,45 +563,91 @@ public class ProfileActivity extends AppCompatActivity { } private void setupWorksRecycler() { - worksAdapter = new UserWorksAdapter(); - worksAdapter.setOnWorkClickListener(workItem -> { - if (workItem != null && !TextUtils.isEmpty(workItem.getId())) { - WorkDetailActivity.start(this, workItem.getId()); + // 设置我的作品区域 + myWorksAdapter = new WorksAdapter(work -> { + if (work != null && work.getId() != null) { + WorkDetailActivity.start(this, String.valueOf(work.getId())); } }); - binding.worksRecycler.setLayoutManager(new GridLayoutManager(this, 3)); - binding.worksRecycler.setAdapter(worksAdapter); - loadWorks(); + binding.myWorksRecycler.setLayoutManager(new GridLayoutManager(this, 2)); + binding.myWorksRecycler.setAdapter(myWorksAdapter); + + // 发布按钮点击事件 + binding.myWorksPublishBtn.setOnClickListener(v -> { + if (!AuthHelper.requireLogin(this, "发布作品需要登录")) { + return; + } + PublishWorkActivity.start(this); + }); + + loadMyWorks(); + } + + private void loadMyWorks() { + if (!AuthHelper.isLoggedIn(this)) { + // 未登录时显示空状态 + binding.myWorksRecycler.setVisibility(View.GONE); + binding.myWorksEmptyState.setVisibility(View.VISIBLE); + binding.myWorksCount.setText("0个作品"); + return; + } + + // 获取当前用户ID + String userIdStr = com.example.livestreaming.net.AuthStore.getUserId(this); + if (userIdStr == null || userIdStr.isEmpty()) { + Log.e(TAG, "无法获取用户ID"); + showMyWorksEmpty(); + return; + } + + int userId; + try { + userId = Integer.parseInt(userIdStr); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userIdStr); + showMyWorksEmpty(); + return; + } + + ApiClient.getService(this).getUserWorks(userId, 1, 50) + .enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null && response.body().isOk()) { + PageResponse pageData = response.body().getData(); + if (pageData != null && pageData.getList() != null && !pageData.getList().isEmpty()) { + List works = pageData.getList(); + binding.myWorksRecycler.setVisibility(View.VISIBLE); + binding.myWorksEmptyState.setVisibility(View.GONE); + binding.myWorksCount.setText(works.size() + "个作品"); + myWorksAdapter.submitList(works); + } else { + showMyWorksEmpty(); + } + } else { + Log.e(TAG, "加载我的作品失败: " + (response.body() != null ? response.body().getMessage() : "未知错误")); + showMyWorksEmpty(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载我的作品失败: " + t.getMessage()); + showMyWorksEmpty(); + } + }); + } + + private void showMyWorksEmpty() { + binding.myWorksRecycler.setVisibility(View.GONE); + binding.myWorksEmptyState.setVisibility(View.VISIBLE); + binding.myWorksCount.setText("0个作品"); } private void loadWorks() { - // TODO: 接入后端接口 - 获取当前用户的作品列表 - // 接口路径: GET /api/users/{userId}/works - // 请求方法: GET - // 请求头: Authorization: Bearer {token} (必填,从AuthStore获取) - // 路径参数: - // - userId: String (必填) - 当前用户ID(从token中解析获取) - // 请求参数(Query): - // - page: int (可选,默认1) - 页码 - // - pageSize: int (可选,默认20) - 每页数量 - // 返回数据格式: ApiResponse> - // 实现步骤: - // 1. 从AuthStore获取token,解析userId(或从用户信息中获取) - // 2. 调用接口获取作品列表 - // 3. 更新UI显示作品列表或空状态 - // 4. 处理错误情况(网络错误、未登录等) - // 注意: 此方法在onResume时也会调用,需要避免重复请求 - - // 临时:从本地存储加载(等待后端接口) - List works = WorkManager.getAllWorks(this); - if (works != null && !works.isEmpty()) { - binding.worksRecycler.setVisibility(View.VISIBLE); - binding.worksEmptyState.setVisibility(View.GONE); - worksAdapter.submitList(works); - } else { - binding.worksRecycler.setVisibility(View.GONE); - binding.worksEmptyState.setVisibility(View.VISIBLE); - } + // 旧方法保留兼容,实际使用loadMyWorks + loadMyWorks(); } private void showTab(int index) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java index b4c88378..eb17a864 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/PublishWorkActivity.java @@ -83,6 +83,10 @@ public class PublishWorkActivity extends AppCompatActivity { private String selectedVisibility = "所有人可见"; // 可见范围 private String selectedCommentSetting = "所有人可评论"; // 评论设置 + // 分类选择相关 + private final List allCategories = new ArrayList<>(); + private com.example.livestreaming.net.CategoryResponse selectedCategory = null; + // 天地图定位服务继续 private TianDiTuLocationService locationService; private ActivityResultLauncher requestLocationPermissionLauncher; @@ -112,6 +116,7 @@ public class PublishWorkActivity extends AppCompatActivity { setupMediaAdapter(); setupLaunchers(); setupClickListeners(); + loadCategories(); // 加载分类数据 } private void setupToolbar() { @@ -347,6 +352,9 @@ public class PublishWorkActivity extends AppCompatActivity { binding.selectCoverButton.setOnClickListener(v -> showCoverPickerDialog()); binding.publishButton.setOnClickListener(v -> publishWork()); + // 分类选择点击事件 + binding.categorySpinner.setOnClickListener(v -> showCategoryPickerDialog()); + // 封面预览点击也可以选择封面 binding.coverPreview.setOnClickListener(v -> { showCoverPickerDialog(); @@ -964,6 +972,11 @@ public class PublishWorkActivity extends AppCompatActivity { commentSettingValue = "DISABLED"; } request.setCommentSetting(commentSettingValue); + + // 设置分类ID + if (selectedCategory != null) { + request.setCategoryId(selectedCategory.getId()); + } ApiService apiService = ApiClient.getService(this); Call> 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>> call = + apiService.getLiveRoomCategories(); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + try { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + allCategories.clear(); + allCategories.addAll(apiResponse.getData()); + android.util.Log.d("PublishWork", "加载分类成功: " + allCategories.size() + " 个分类"); + } else { + android.util.Log.w("PublishWork", "分类数据为空或API返回错误"); + } + } else { + android.util.Log.e("PublishWork", "加载分类失败: " + response.code()); + } + } catch (Exception e) { + android.util.Log.e("PublishWork", "处理分类数据异常", e); + } + } + @Override + public void onFailure(Call>> call, Throwable t) { + android.util.Log.e("PublishWork", "加载分类网络错误", t); + } + }); + } + + /** + * 显示分类选择对话框(单选) + */ + private void showCategoryPickerDialog() { + if (allCategories.isEmpty()) { + Toast.makeText(this, "分类数据加载中,请稍后再试", Toast.LENGTH_SHORT).show(); + return; + } + + // 创建分类名称数组 + String[] categoryNames = new String[allCategories.size()]; + int selectedIndex = -1; + + for (int i = 0; i < allCategories.size(); i++) { + com.example.livestreaming.net.CategoryResponse category = allCategories.get(i); + categoryNames[i] = category.getName(); + // 检查是否是当前选中的分类 + if (selectedCategory != null && selectedCategory.getId().equals(category.getId())) { + selectedIndex = i; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("选择分类"); + + builder.setSingleChoiceItems(categoryNames, selectedIndex, (dialog, which) -> { + selectedCategory = allCategories.get(which); + updateCategoryDisplay(); + Toast.makeText(this, "已选择分类:" + selectedCategory.getName(), Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + builder.setNegativeButton("取消", null); + + builder.setNeutralButton("清空", (dialog, which) -> { + selectedCategory = null; + updateCategoryDisplay(); + Toast.makeText(this, "已清空分类选择", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + builder.show(); + } + + /** + * 更新分类显示 + */ + private void updateCategoryDisplay() { + if (selectedCategory == null) { + binding.categorySpinner.setText(""); + binding.categorySpinner.setHint("请选择分类"); + } else { + binding.categorySpinner.setText(selectedCategory.getName()); + } + }} diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java index def4ea9a..0effe316 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java @@ -23,6 +23,8 @@ import com.example.livestreaming.net.HotSearchResponse; import com.example.livestreaming.net.PageResponse; import com.example.livestreaming.net.ApiClient; import com.example.livestreaming.net.Room; +import com.example.livestreaming.net.WorksResponse; +import com.example.livestreaming.net.SearchUserResponse; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.tabs.TabLayout; @@ -47,10 +49,14 @@ public class SearchActivity extends AppCompatActivity { // 主播列表 private SearchStreamerAdapter streamersAdapter; private final List> streamersList = new ArrayList<>(); + + // 作品列表 + private WorksAdapter worksAdapter; + private final List worksList = new ArrayList<>(); private boolean isSearching = false; private String lastSearchKeyword = ""; - private int currentTab = 0; // 0=直播间, 1=主播 + private int currentTab = 0; // 0=直播间, 1=主播, 2=作品 private static final String EXTRA_SEARCH_QUERY = "search_query"; @@ -118,11 +124,11 @@ public class SearchActivity extends AppCompatActivity { streamersAdapter.setOnStreamerClickListener(new SearchStreamerAdapter.OnStreamerClickListener() { @Override public void onStreamerClick(Map streamer) { - // 点击主播,跳转到主播主页 + // 点击用户,跳转到用户主页 Object id = streamer.get("id"); if (id != null) { - int streamerId = ((Number) id).intValue(); - UserProfileActivity.start(SearchActivity.this, streamerId); + int userId = ((Number) id).intValue(); + UserProfileReadOnlyActivity.start(SearchActivity.this, userId); } } @@ -135,12 +141,24 @@ public class SearchActivity extends AppCompatActivity { binding.streamersRecyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.streamersRecyclerView.setAdapter(streamersAdapter); + + // 作品适配器 + worksAdapter = new WorksAdapter(work -> { + if (work == null) return; + WorkDetailActivity.start(SearchActivity.this, String.valueOf(work.getId())); + }); + + StaggeredGridLayoutManager worksLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); + worksLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); + binding.worksRecyclerView.setLayoutManager(worksLayoutManager); + binding.worksRecyclerView.setAdapter(worksAdapter); } private void setupTabs() { // 添加Tab binding.searchTabs.addTab(binding.searchTabs.newTab().setText("直播间")); - binding.searchTabs.addTab(binding.searchTabs.newTab().setText("主播")); + binding.searchTabs.addTab(binding.searchTabs.newTab().setText("用户")); + binding.searchTabs.addTab(binding.searchTabs.newTab().setText("作品")); binding.searchTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override @@ -216,6 +234,149 @@ public class SearchActivity extends AppCompatActivity { binding.hotSearchContainer.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE); + // 清空之前的结果 + roomsList.clear(); + streamersList.clear(); + worksList.clear(); + + ApiService apiService = ApiClient.getService(this); + + // 同时搜索直播间+用户和作品 + final String searchKeyword = keyword; + final int[] completedCount = {0}; + final int totalRequests = 2; + + // 搜索直播间和用户(使用原来的综合搜索API) + Call>> roomsCall = + apiService.comprehensiveSearch(searchKeyword, 1, 20); + roomsCall.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map data = apiResponse.getData(); + + // 解析直播间列表 + Object roomsObj = data.get("rooms"); + if (roomsObj instanceof List) { + List rooms = (List) roomsObj; + for (Object item : rooms) { + if (item instanceof Map) { + Room room = parseRoomFromMap((Map) item); + if (room != null) { + roomsList.add(room); + } + } + } + } + + // 解析用户列表(使用原来的streamers字段) + Object streamersObj = data.get("streamers"); + if (streamersObj instanceof List) { + List streamers = (List) streamersObj; + for (Object item : streamers) { + if (item instanceof Map) { + streamersList.add((Map) item); + } + } + } + } + } + + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "搜索直播间和用户失败", t); + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + }); + + // 搜索作品 + Map worksRequest = new HashMap<>(); + worksRequest.put("keyword", searchKeyword); + worksRequest.put("page", 1); + worksRequest.put("pageSize", 20); + + Call>> worksCall = + apiService.searchWorks(worksRequest); + worksCall.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List works = apiResponse.getData().getList(); + if (works != null) { + worksList.addAll(works); + } + } + } + + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "搜索作品失败", t); + completedCount[0]++; + if (completedCount[0] >= totalRequests) { + onSearchCompleted(); + } + } + }); + } + + /** + * 搜索完成后的处理 + */ + private void onSearchCompleted() { + isSearching = false; + binding.loadingProgress.setVisibility(View.GONE); + + int roomsTotal = roomsList.size(); + int usersTotal = streamersList.size(); + int worksTotal = worksList.size(); + + Log.d(TAG, "搜索完成,直播间: " + roomsTotal + ", 用户: " + usersTotal + ", 作品: " + worksTotal); + + // 更新Tab标题显示数量 + updateTabTitles(roomsTotal, usersTotal, worksTotal); + + // 显示搜索结果 + showSearchResults(); + } + + /** + * 执行综合搜索(旧方法,保留备用) + */ + private void performSearchOld(String keyword) { + if (keyword == null || keyword.trim().isEmpty()) { + return; + } + + keyword = keyword.trim(); + + Log.d(TAG, "执行综合搜索: " + keyword); + + // 显示加载状态 + binding.loadingProgress.setVisibility(View.VISIBLE); + binding.hotSearchContainer.setVisibility(View.GONE); + binding.emptyStateView.setVisibility(View.GONE); + ApiService apiService = ApiClient.getService(this); Call>> call = apiService.comprehensiveSearch(keyword, 1, 20); @@ -263,11 +424,12 @@ public class SearchActivity extends AppCompatActivity { // 获取总数 int roomsTotal = getIntValue(data.get("roomsTotal"), 0); int streamersTotal = getIntValue(data.get("streamersTotal"), 0); + int worksTotal = 0; // 旧方法没有作品数据 Log.d(TAG, "搜索成功,直播间: " + roomsTotal + ", 主播: " + streamersTotal); // 更新Tab标题显示数量 - updateTabTitles(roomsTotal, streamersTotal); + updateTabTitles(roomsTotal, streamersTotal, worksTotal); // 显示搜索结果 showSearchResults(); @@ -301,15 +463,19 @@ public class SearchActivity extends AppCompatActivity { /** * 更新Tab标题显示数量 */ - private void updateTabTitles(int roomsCount, int streamersCount) { + private void updateTabTitles(int roomsCount, int streamersCount, int worksCount) { TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); + TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2); if (roomsTab != null) { roomsTab.setText("直播间 " + roomsCount); } if (streamersTab != null) { - streamersTab.setText("主播 " + streamersCount); + streamersTab.setText("用户 " + streamersCount); + } + if (worksTab != null) { + worksTab.setText("作品 " + worksCount); } } @@ -323,6 +489,7 @@ public class SearchActivity extends AppCompatActivity { // 更新列表数据 roomsAdapter.submitList(new ArrayList<>(roomsList)); streamersAdapter.submitList(new ArrayList<>(streamersList)); + worksAdapter.submitList(new ArrayList<>(worksList)); // 根据当前Tab显示对应内容 updateTabContent(); @@ -336,6 +503,7 @@ public class SearchActivity extends AppCompatActivity { // 显示直播间 binding.roomsRecyclerView.setVisibility(View.VISIBLE); binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.GONE); if (roomsList.isEmpty()) { binding.emptyStateView.setNoSearchResultsState(); @@ -343,10 +511,11 @@ public class SearchActivity extends AppCompatActivity { } else { binding.emptyStateView.setVisibility(View.GONE); } - } else { + } else if (currentTab == 1) { // 显示主播 binding.roomsRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.VISIBLE); + binding.worksRecyclerView.setVisibility(View.GONE); if (streamersList.isEmpty()) { binding.emptyStateView.setNoSearchResultsState(); @@ -354,6 +523,18 @@ public class SearchActivity extends AppCompatActivity { } else { binding.emptyStateView.setVisibility(View.GONE); } + } else { + // 显示作品 + binding.roomsRecyclerView.setVisibility(View.GONE); + binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.VISIBLE); + + if (worksList.isEmpty()) { + binding.emptyStateView.setNoSearchResultsState(); + binding.emptyStateView.setVisibility(View.VISIBLE); + } else { + binding.emptyStateView.setVisibility(View.GONE); + } } } @@ -364,14 +545,17 @@ public class SearchActivity extends AppCompatActivity { binding.searchTabs.setVisibility(View.GONE); binding.roomsRecyclerView.setVisibility(View.GONE); binding.streamersRecyclerView.setVisibility(View.GONE); + binding.worksRecyclerView.setVisibility(View.GONE); binding.emptyStateView.setVisibility(View.GONE); binding.hotSearchContainer.setVisibility(View.VISIBLE); // 重置Tab标题 TabLayout.Tab roomsTab = binding.searchTabs.getTabAt(0); TabLayout.Tab streamersTab = binding.searchTabs.getTabAt(1); + TabLayout.Tab worksTab = binding.searchTabs.getTabAt(2); if (roomsTab != null) roomsTab.setText("直播间"); - if (streamersTab != null) streamersTab.setText("主播"); + if (streamersTab != null) streamersTab.setText("用户"); + if (worksTab != null) worksTab.setText("作品"); } /** @@ -451,21 +635,21 @@ public class SearchActivity extends AppCompatActivity { * 处理关注点击 */ private void handleFollowClick(Map streamer, int position) { - if (!AuthHelper.requireLogin(this, "关注主播需要登录")) { + if (!AuthHelper.requireLogin(this, "关注用户需要登录")) { return; } Object id = streamer.get("id"); if (id == null) return; - int streamerId = ((Number) id).intValue(); + int userId = ((Number) id).intValue(); Object isFollowing = streamer.get("isFollowing"); boolean following = isFollowing instanceof Boolean && (Boolean) isFollowing; String action = following ? "unfollow" : "follow"; Map body = new HashMap<>(); - body.put("followedId", streamerId); + body.put("userId", userId); ApiService apiService = ApiClient.getService(this); Call>> call = following ? diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java index bcb39b2c..a0528f9a 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java @@ -68,6 +68,15 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { context.startActivity(intent); } + /** + * 只传userId启动,其他信息从后端获取 + */ + public static void start(Context context, int userId) { + Intent intent = new Intent(context, UserProfileReadOnlyActivity.class); + intent.putExtra(EXTRA_USER_ID, String.valueOf(userId)); + context.startActivity(intent); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -122,6 +131,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { // 检查关注状态 checkFollowStatus(); + // 初始化关注按钮显示(默认显示"关注") + updateFollowButton(); + // 检查好友状态 checkFriendStatus(); @@ -285,11 +297,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { if (isFollowing) { binding.followButton.setText("已关注"); binding.followButton.setSelected(true); + binding.followButton.setBackgroundResource(R.drawable.bg_follow_button_followed); binding.followButton.setTextColor(getResources().getColor(android.R.color.darker_gray, null)); } else { binding.followButton.setText("关注"); binding.followButton.setSelected(false); - binding.followButton.setTextColor(getResources().getColor(R.color.red_primary, null)); + binding.followButton.setBackgroundResource(R.drawable.bg_follow_button_normal); + binding.followButton.setTextColor(0xFF333333); } } @@ -502,7 +516,168 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { private void bindDemoStatsAndWorks(String userId) { if (binding == null) return; - String seed = !TextUtils.isEmpty(userId) ? userId : "demo"; + // 如果只传了userId,需要从后端获取用户信息 + if (!TextUtils.isEmpty(userId)) { + loadUserProfile(userId); + loadUserWorks(userId); + } else { + // 没有userId,显示演示数据 + showDemoData(); + } + } + + /** + * 从后端加载用户主页信息 + */ + private void loadUserProfile(String userId) { + try { + int uid = Integer.parseInt(userId); + ApiService apiService = ApiClient.getService(this); + Call>> call = apiService.getUserProfile(uid); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Map data = apiResponse.getData(); + + // 更新用户名 + String nickname = (String) data.get("nickname"); + if (!TextUtils.isEmpty(nickname)) { + currentUserName = nickname; + binding.name.setText(nickname); + } + + // 更新头像 + String avatar = (String) data.get("avatar"); + if (!TextUtils.isEmpty(avatar)) { + com.bumptech.glide.Glide.with(UserProfileReadOnlyActivity.this) + .load(avatar) + .placeholder(R.drawable.wish_tree_checker_backup) + .error(R.drawable.wish_tree_checker_backup) + .circleCrop() + .into(binding.avatar); + } + + // 更新统计数据 + Object worksCount = data.get("worksCount"); + Object followingCount = data.get("followingCount"); + Object followersCount = data.get("followersCount"); + Object likesCount = data.get("likesCount"); + + if (worksCount != null) { + binding.statWorksValue.setText(formatIntValue(worksCount)); + } + if (followingCount != null) { + binding.statFollowingValue.setText(formatIntValue(followingCount)); + } + if (followersCount != null) { + binding.statFollowersValue.setText(formatIntValue(followersCount)); + } + if (likesCount != null) { + binding.statLikesValue.setText(formatIntValue(likesCount)); + } + + // 更新签名 + String mark = (String) data.get("mark"); + if (!TextUtils.isEmpty(mark)) { + binding.bio.setText(mark); + } + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载用户信息失败", t); + } + }); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userId); + } + } + + /** + * 从后端加载用户作品列表 + */ + private void loadUserWorks(String userId) { + try { + int uid = Integer.parseInt(userId); + ApiService apiService = ApiClient.getService(this); + Call>> call = + apiService.getUserWorks(uid, 1, 50); + + call.enqueue(new Callback>>() { + @Override + public void onResponse(Call>> call, + Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + List worksList = apiResponse.getData().getList(); + + if (worksList != null && !worksList.isEmpty()) { + // 转换为WorkItem列表 + List works = new ArrayList<>(); + for (com.example.livestreaming.net.WorksResponse wr : worksList) { + WorkItem work = new WorkItem(); + work.setId(String.valueOf(wr.getId())); + work.setTitle(wr.getTitle()); + work.setDescription(wr.getDescription()); + work.setCoverUrl(wr.getCoverUrl()); + work.setVideoUrl(wr.getVideoUrl()); + work.setImageUrls(wr.getImageUrls()); + work.setLikeCount(wr.getLikeCount()); + work.setCollectCount(wr.getCollectCount()); + work.setViewCount(wr.getViewCount()); + work.setType("VIDEO".equals(wr.getType()) ? WorkItem.WorkType.VIDEO : WorkItem.WorkType.IMAGE); + works.add(work); + } + + // 更新作品数量 + binding.statWorksValue.setText(String.valueOf(works.size())); + + // 设置适配器 + if (worksAdapter != null) { + worksAdapter.setOnWorkClickListener(workItem -> { + if (workItem != null && !TextUtils.isEmpty(workItem.getId())) { + WorkDetailActivity.start(UserProfileReadOnlyActivity.this, workItem.getId()); + } + }); + worksAdapter.submitList(works); + } + } else { + // 没有作品,显示空状态 + if (worksAdapter != null) { + worksAdapter.submitList(new ArrayList<>()); + } + } + } else { + Log.e(TAG, "获取作品失败: " + (apiResponse.getMessage() != null ? apiResponse.getMessage() : "未知错误")); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + Log.e(TAG, "加载用户作品失败", t); + Toast.makeText(UserProfileReadOnlyActivity.this, "加载作品失败", Toast.LENGTH_SHORT).show(); + } + }); + } catch (NumberFormatException e) { + Log.e(TAG, "用户ID格式错误: " + userId); + Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 显示演示数据(当没有userId时) + */ + private void showDemoData() { + String seed = "demo"; int h = Math.abs(seed.hashCode()); int worksCount = 6 + (h % 18); @@ -517,16 +692,9 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { // 创建演示作品列表 List works = new ArrayList<>(); - int[] pool = new int[] { - R.drawable.wish_tree_checker_backup, - R.drawable.wish_tree_prev_no_bg, - R.drawable.wish_tree_trim_backup, - R.drawable.wish_tree_black, - R.drawable.wish_tree - }; for (int i = 0; i < 12; i++) { WorkItem work = new WorkItem(); - work.setId("demo_" + userId + "_" + i); + work.setId("demo_" + i); work.setTitle("演示作品 " + (i + 1)); work.setType(WorkItem.WorkType.IMAGE); work.setLikeCount((h + i) % 100); @@ -643,4 +811,23 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity { } }); } + + /** + * 将数值转换为整数字符串显示 + */ + private String formatIntValue(Object value) { + if (value == null) { + return "0"; + } + try { + if (value instanceof Number) { + return String.valueOf(((Number) value).intValue()); + } + // 尝试解析字符串 + double d = Double.parseDouble(value.toString()); + return String.valueOf((int) d); + } catch (Exception e) { + return "0"; + } + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java index a44f7475..b956bc01 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java @@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog; import java.util.ArrayList; import java.util.List; +import java.util.Map; import retrofit2.Call; @@ -44,6 +45,7 @@ public class WorkDetailActivity extends AppCompatActivity { // 状态标记 private boolean isLiked = false; private boolean isFavorited = false; + private boolean isFollowed = false; // 是否已关注作者 private int commentCount = 0; private static final String EXTRA_WORK_ID = "work_id"; @@ -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>> call = apiService.checkFollowStatus(userId); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200 && apiResponse.getData() != null) { + Object isFollowingObj = apiResponse.getData().get("isFollowing"); + isFollowed = isFollowingObj != null && Boolean.parseBoolean(isFollowingObj.toString()); + updateFollowButton(); + } + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + android.util.Log.e("WorkDetail", "检查关注状态失败", t); + } + }); + } + + /** + * 更新关注按钮显示 + */ + private void updateFollowButton() { + if (isFollowed) { + // 已关注,隐藏+号按钮 + binding.followButton.setVisibility(View.GONE); + } else { + // 未关注,显示+号按钮 + binding.followButton.setVisibility(View.VISIBLE); + } + } + + /** + * 切换关注状态 + */ + private void toggleFollow(int userId) { + // 检查登录状态 + if (!AuthHelper.requireLogin(this, "关注需要登录")) { + return; + } + + if (userId <= 0) { + Toast.makeText(this, "用户信息无效", Toast.LENGTH_SHORT).show(); + return; + } + + ApiService apiService = ApiClient.getService(this); + + // 构建请求参数 + java.util.Map body = new java.util.HashMap<>(); + body.put("userId", userId); + + Call>> call; + if (isFollowed) { + // 取消关注 + call = apiService.unfollowUser(body); + } else { + // 关注 + call = apiService.followUser(body); + } + + // 乐观更新UI + boolean oldFollowed = isFollowed; + isFollowed = !isFollowed; + updateFollowButton(); + + call.enqueue(new retrofit2.Callback>>() { + @Override + public void onResponse(Call>> call, + retrofit2.Response>> response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse> apiResponse = response.body(); + if (apiResponse.getCode() == 200) { + Toast.makeText(WorkDetailActivity.this, + isFollowed ? "关注成功" : "已取消关注", + Toast.LENGTH_SHORT).show(); + } else { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, + apiResponse.getMessage() != null ? apiResponse.getMessage() : "操作失败", + Toast.LENGTH_SHORT).show(); + } + } else { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, "操作失败", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call>> call, Throwable t) { + // 恢复原状态 + isFollowed = oldFollowed; + updateFollowButton(); + Toast.makeText(WorkDetailActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + private void setupActionButton() { // ============================================ // TODO: 判断是否是当前用户的作品 @@ -1051,6 +1229,7 @@ public class WorkDetailActivity extends AppCompatActivity { setupContent(); setupActionButtons(); setupActionButton(); + setupAuthorSection(); // 设置作者头像和关注按钮 // 获取真实的评论数量 loadRealCommentCount(worksResponse.getId()); @@ -1099,6 +1278,11 @@ public class WorkDetailActivity extends AppCompatActivity { // 防止 publishTime 为 null 导致 NullPointerException item.setPublishTime(response.getPublishTime() != null ? response.getPublishTime() : 0L); + // 设置作者信息 + item.setAuthorId(response.getUserId() != null ? response.getUserId() : 0); + item.setAuthorName(response.getAuthorName() != null ? response.getAuthorName() : response.getUserName()); + item.setAuthorAvatar(response.getAuthorAvatar() != null ? response.getAuthorAvatar() : response.getUserAvatar()); + // 设置作品类型 if ("VIDEO".equals(response.getType())) { item.setType(WorkItem.WorkType.VIDEO); diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java b/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java index 344134fa..d5f9baed 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorkItem.java @@ -23,6 +23,11 @@ public class WorkItem implements Parcelable { private long publishTime; private WorkType type; // 作品类型:图片或视频 + // 作者信息 + private int authorId; // 作者用户ID + private String authorName; // 作者昵称 + private String authorAvatar; // 作者头像URL + // 本地使用的URI(发布时使用) private transient Uri coverUri; private transient Uri videoUri; @@ -41,6 +46,9 @@ public class WorkItem implements Parcelable { this.publishTime = System.currentTimeMillis(); this.imageUrls = new ArrayList<>(); this.imageUris = new ArrayList<>(); + this.authorId = 0; + this.authorName = ""; + this.authorAvatar = ""; } public WorkItem(String id, String title, String description, String coverUrl, @@ -171,6 +179,30 @@ public class WorkItem implements Parcelable { this.imageUris = imageUris; } + public int getAuthorId() { + return authorId; + } + + public void setAuthorId(int authorId) { + this.authorId = authorId; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorAvatar() { + return authorAvatar; + } + + public void setAuthorAvatar(String authorAvatar) { + this.authorAvatar = authorAvatar; + } + // Parcelable implementation protected WorkItem(Parcel in) { id = in.readString(); @@ -186,6 +218,9 @@ public class WorkItem implements Parcelable { int typeOrdinal = in.readInt(); type = typeOrdinal >= 0 && typeOrdinal < WorkType.values().length ? WorkType.values()[typeOrdinal] : WorkType.IMAGE; + authorId = in.readInt(); + authorName = in.readString(); + authorAvatar = in.readString(); } @Override @@ -201,6 +236,9 @@ public class WorkItem implements Parcelable { dest.writeInt(viewCount); dest.writeLong(publishTime); dest.writeInt(type != null ? type.ordinal() : -1); + dest.writeInt(authorId); + dest.writeString(authorName); + dest.writeString(authorAvatar); } @Override diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java index 0594e2fe..daa04224 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.java @@ -377,6 +377,23 @@ public interface ApiService { @GET("api/front/works/detail/{id}") Call> getWorkDetail(@Path("id") long id); + /** + * 获取当前用户发布的作品列表 + */ + @GET("api/front/works/my") + Call>> getMyPublishedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取指定用户发布的作品列表 + */ + @GET("api/front/works/user/{userId}") + Call>> getUserWorks( + @Path("userId") int userId, + @Query("page") int page, + @Query("pageSize") int pageSize); + @DELETE("api/front/works/delete/{id}") Call> deleteWork(@Path("id") long id); @@ -979,4 +996,28 @@ public interface ApiService { @Query("categoryId") Integer categoryId, @Query("page") int page, @Query("limit") int limit); + + /** + * 搜索作品(使用POST方法) + */ + @POST("api/front/works/search") + Call>> searchWorks(@Body Map request); + + // ==================== 我的点赞/收藏作品接口 ==================== + + /** + * 获取我点赞的作品列表 + */ + @GET("api/front/works/my/liked") + Call>> getMyLikedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); + + /** + * 获取我收藏的作品列表 + */ + @GET("api/front/works/my/collected") + Call>> getMyCollectedWorks( + @Query("page") int page, + @Query("pageSize") int pageSize); } diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag.xml b/android-app/app/src/main/res/drawable/bg_channel_tag.xml index cb5ed321..c303e63d 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag.xml @@ -1,9 +1,9 @@ + + - - diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml index a0e6bba4..c303e63d 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_normal.xml @@ -1,9 +1,9 @@ - - + + + android:color="#E0E0E0" /> diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml new file mode 100644 index 00000000..0ee07b0f --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_recommend.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml b/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml index 2d0c3668..4f560150 100644 --- a/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml +++ b/android-app/app/src/main/res/drawable/bg_channel_tag_selected.xml @@ -1,9 +1,9 @@ + + - - + android:width="2dp" + android:color="#1976D2" /> diff --git a/android-app/app/src/main/res/drawable/bg_circle_white.xml b/android-app/app/src/main/res/drawable/bg_circle_white.xml new file mode 100644 index 00000000..15067aa6 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_circle_white.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/bg_follow_button.xml b/android-app/app/src/main/res/drawable/bg_follow_button.xml index 4fc40821..ed56210b 100644 --- a/android-app/app/src/main/res/drawable/bg_follow_button.xml +++ b/android-app/app/src/main/res/drawable/bg_follow_button.xml @@ -1,19 +1,8 @@ - - - - - - - - - - - - - - - - - - + + + + diff --git a/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml b/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml new file mode 100644 index 00000000..06bd22e3 --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_follow_button_followed.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml b/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml new file mode 100644 index 00000000..0abc92db --- /dev/null +++ b/android-app/app/src/main/res/drawable/bg_follow_button_normal.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_my_collections.xml b/android-app/app/src/main/res/layout/activity_my_collections.xml new file mode 100644 index 00000000..93dec2b2 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_my_collections.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_my_likes.xml b/android-app/app/src/main/res/layout/activity_my_likes.xml new file mode 100644 index 00000000..66b50d07 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_my_likes.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_profile.xml b/android-app/app/src/main/res/layout/activity_profile.xml index df1436c8..d5d6af97 100644 --- a/android-app/app/src/main/res/layout/activity_profile.xml +++ b/android-app/app/src/main/res/layout/activity_profile.xml @@ -70,6 +70,7 @@ android:layout_marginTop="10dp" android:gravity="center_vertical" android:orientation="horizontal" + android:visibility="gone" app:layout_constraintHorizontal_bias="0" app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintStart_toStartOf="@id/name" @@ -187,6 +188,7 @@ android:background="@drawable/bg_circle_white_60" android:padding="8dp" android:src="@drawable/ic_clock_24" + android:visibility="gone" app:layout_constraintEnd_toStartOf="@id/topActionMore" app:layout_constraintTop_toTopOf="@id/topActionMore" /> @@ -198,6 +200,7 @@ android:background="@drawable/bg_circle_white_60" android:padding="8dp" android:src="@drawable/ic_crosshair_24" + android:visibility="gone" app:layout_constraintEnd_toStartOf="@id/topActionClock" app:layout_constraintTop_toTopOf="@id/topActionMore" /> @@ -439,7 +442,7 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" - android:src="@drawable/ic_like_24" + android:src="@drawable/ic_like_filled_24" android:tint="#FF4081" /> @@ -453,7 +456,7 @@ @@ -488,8 +491,8 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" - android:src="@drawable/ic_heart_24" - android:tint="#E91E63" /> + android:src="@drawable/ic_star_24" + android:tint="#FFA726" /> @@ -502,7 +505,7 @@ @@ -512,7 +515,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="2dp" - android:text="0人" + android:text="0个" android:textColor="#999999" android:textSize="11sp" /> @@ -525,7 +528,8 @@ android:layout_width="wrap_content" android:layout_height="64dp" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:visibility="gone"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/myWorksSection"> + + + + + + + + + + + + + + + + + + + + + @@ -271,7 +271,7 @@ android:id="@+id/space_addFriend_margin_top" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintHeight_percent="0.028" + app:layout_constraintHeight_percent="0.015" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/bio" /> @@ -292,10 +292,10 @@ android:layout_height="44dp" android:layout_weight="1" android:layout_marginEnd="6dp" - android:background="@drawable/bg_follow_button" + android:background="@drawable/bg_follow_button_normal" android:gravity="center" android:text="关注" - android:textColor="#FF4757" + android:textColor="#333333" android:textSize="15sp" android:textStyle="bold" /> diff --git a/android-app/app/src/main/res/layout/activity_work_detail.xml b/android-app/app/src/main/res/layout/activity_work_detail.xml index a4f82a51..a702e4cb 100644 --- a/android-app/app/src/main/res/layout/activity_work_detail.xml +++ b/android-app/app/src/main/res/layout/activity_work_detail.xml @@ -80,7 +80,7 @@ - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android-app/app/src/main/res/layout/dialog_category_management.xml b/android-app/app/src/main/res/layout/dialog_category_management.xml new file mode 100644 index 00000000..ba9be380 --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_category_management.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/fragment_list_content.xml b/android-app/app/src/main/res/layout/fragment_list_content.xml new file mode 100644 index 00000000..29491a25 --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_list_content.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_channel_tag.xml b/android-app/app/src/main/res/layout/item_channel_tag.xml index b57f7da3..2619c6d6 100644 --- a/android-app/app/src/main/res/layout/item_channel_tag.xml +++ b/android-app/app/src/main/res/layout/item_channel_tag.xml @@ -3,52 +3,31 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="4dp"> + android:layout_margin="6dp"> + android:textSize="13sp" + android:textColor="#666666" + android:background="@drawable/bg_channel_tag_normal" + android:minWidth="60dp" + android:gravity="center" /> - + - - - - - - diff --git a/android-app/搜索功能更新说明.md b/android-app/搜索功能更新说明.md new file mode 100644 index 00000000..af76f9f6 --- /dev/null +++ b/android-app/搜索功能更新说明.md @@ -0,0 +1,63 @@ +# 搜索功能和分类管理修复说明 + +## 一、搜索功能修复 + +### 1. 用户搜索问题修复 ✅ +**问题**:用户搜索结果始终为0 +**修复**:恢复使用 `comprehensiveSearch` API,从 `streamers` 字段获取用户数据 + +### 2. 作品搜索问题修复 ✅ +**问题**:搜索结果不准确 +**修复**:使用正确的 `POST /api/front/works/search` 接口 + +## 二、分类管理功能 + +### 1. 后端修改 +**文件**:`CategoryController.java` +- 修改 `getWorkCategories()` 方法,让作品分类使用直播间分类 +- 实现统一的分类系统(直播和作品使用相同分类) + +### 2. Android端修改 +**文件**:`MainActivity.java` +- 添加了下拉按钮(btnExpandCategories)点击事件 +- 添加了 `showCategoryManagementDialog()` 方法 +- 添加了 `setupCategoryManagementDialog()` 方法 +- 添加了 `loadCategoriesForDialog()` 方法 +- 添加了 `updateCategoryTabsFromMyChannels()` 方法 + +**文件**:`dialog_category_management.xml` +- 创建了分类管理对话框布局 +- 包含"我的频道"和"推荐频道"两个区域 +- 支持添加/移除频道 + +## 三、数据库说明 + +### 现有数据 +- **eb_live_room_category**:有5个分类(娱乐、游戏、音乐、户外、聊天) +- **eb_category**:type=8或9的数据为空 +- **eb_works**:作品的category_id都为空 + +### 统一分类方案 +- 直播和作品都使用 `eb_live_room_category` 表的分类 +- 后端 `getWorkCategories()` 接口返回直播间分类 + +## 四、测试建议 + +1. **搜索功能测试**: + - 测试用户搜索 + - 测试作品搜索(按标题模糊匹配) + - 测试直播间搜索 + +2. **分类管理测试**: + - 点击首页右上角下拉按钮 + - 验证分类管理对话框显示 + - 测试添加/移除频道功能 + +## 五、完成状态 + +✅ 搜索功能修复完成 +✅ 后端分类统一完成 +✅ 下拉按钮点击事件添加完成 +✅ 分类管理对话框创建完成 + +**可以重新编译测试了!**