Compare commits
No commits in common. "7b7ca4f405bbec831e5cebe795026efe68b4a6c7" and "987e67dcb0149ebca0cd41388ea81f99b84595b5" have entirely different histories.
7b7ca4f405
...
987e67dcb0
|
|
@ -1,578 +0,0 @@
|
|||
# Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
本设计文档描述了在Android直播应用中添加"热门"Tab功能的技术实现方案。该功能将在"发现"页面的分类标签栏(categoryTabs)中添加一个"热门"Tab,位于最左侧("推荐"Tab左边),用于展示后台管理员标记为热门的作品内容。
|
||||
|
||||
## Architecture
|
||||
|
||||
### 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MainActivity │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ topTabs: 关注 | 发现 | 附近 │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ categoryTabs: 热门 | 推荐 | 直播 | 视频 | 音乐 | 游戏│ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ RecyclerView (WorksAdapter) │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ 作品1 │ │ 作品2 │ │ 作品3 │ │ │
|
||||
│ │ │ 🔥热门 │ │ 🔥热门 │ │ 🔥热门 │ │ │
|
||||
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
ApiService
|
||||
↓
|
||||
GET /api/works/hot?page=1&pageSize=20
|
||||
↓
|
||||
后端服务器
|
||||
↓
|
||||
返回 is_hot=1 的作品列表
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
1. **用户操作** → 点击"热门"Tab
|
||||
2. **MainActivity** → 调用 `loadHotWorks(page)` 方法
|
||||
3. **ApiService** → 发送 GET 请求到 `/api/works/hot`
|
||||
4. **后端服务器** → 查询 `is_hot=1` 的作品,按 `hot_time` 降序排序
|
||||
5. **ApiService** → 接收 `PageResponse<WorksResponse>` 数据
|
||||
6. **WorksAdapter** → 渲染作品列表
|
||||
7. **UI** → 显示热门作品,每个作品卡片显示🔥标识
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. MainActivity 修改
|
||||
|
||||
#### 新增成员变量
|
||||
|
||||
```java
|
||||
// 热门作品相关
|
||||
private final List<WorksResponse> hotWorks = new ArrayList<>(); // 热门作品列表
|
||||
private int currentHotWorksPage = 1; // 当前热门作品页码
|
||||
private boolean isLoadingHotWorks = false; // 是否正在加载热门作品
|
||||
private boolean hasMoreHotWorks = true; // 是否还有更多热门作品
|
||||
```
|
||||
|
||||
#### 新增方法
|
||||
|
||||
```java
|
||||
/**
|
||||
* 加载热门作品列表
|
||||
* @param page 页码(从1开始)
|
||||
*/
|
||||
private void loadHotWorks(int page) {
|
||||
if (isLoadingHotWorks) return;
|
||||
|
||||
isLoadingHotWorks = true;
|
||||
|
||||
// 显示加载状态
|
||||
if (page == 1) {
|
||||
if (hotWorks.isEmpty()) {
|
||||
LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6);
|
||||
}
|
||||
}
|
||||
|
||||
ApiClient.getService(getApplicationContext())
|
||||
.getHotWorks(page, 20)
|
||||
.enqueue(new Callback<ApiResponse<PageResponse<WorksResponse>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
|
||||
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
|
||||
isLoadingHotWorks = false;
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||||
PageResponse<WorksResponse> pageData = response.body().getData();
|
||||
if (pageData != null && pageData.getList() != null) {
|
||||
if (page == 1) {
|
||||
hotWorks.clear();
|
||||
}
|
||||
hotWorks.addAll(pageData.getList());
|
||||
|
||||
// 检查是否还有更多数据
|
||||
hasMoreHotWorks = pageData.getList().size() >= 20;
|
||||
|
||||
// 更新UI
|
||||
updateHotWorksUI();
|
||||
}
|
||||
} else {
|
||||
showErrorState("加载失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<WorksResponse>>> call, Throwable t) {
|
||||
isLoadingHotWorks = false;
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
showErrorState("网络错误,请重试");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新热门作品UI
|
||||
*/
|
||||
private void updateHotWorksUI() {
|
||||
if (hotWorks.isEmpty()) {
|
||||
showEmptyState("暂无热门作品");
|
||||
} else {
|
||||
hideEmptyState();
|
||||
hideErrorState();
|
||||
|
||||
// 转换为FeedItem列表
|
||||
List<FeedItem> feedItems = new ArrayList<>();
|
||||
for (WorksResponse work : hotWorks) {
|
||||
feedItems.add(FeedItem.fromWorks(work));
|
||||
}
|
||||
adapter.submitList(feedItems);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新热门作品
|
||||
*/
|
||||
private void refreshHotWorks() {
|
||||
currentHotWorksPage = 1;
|
||||
hasMoreHotWorks = true;
|
||||
loadHotWorks(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多热门作品
|
||||
*/
|
||||
private void loadMoreHotWorks() {
|
||||
if (!isLoadingHotWorks && hasMoreHotWorks) {
|
||||
currentHotWorksPage++;
|
||||
loadHotWorks(currentHotWorksPage);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 修改 categoryTabs 初始化
|
||||
|
||||
在 `activity_main.xml` 中的 `categoryTabs` 添加"热门"Tab:
|
||||
|
||||
```xml
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/categoryTabs"
|
||||
...>
|
||||
|
||||
<!-- 新增:热门Tab -->
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="热门" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="推荐" />
|
||||
|
||||
<!-- 其他Tab保持不变 -->
|
||||
...
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
```
|
||||
|
||||
#### 修改 categoryTabs 监听器
|
||||
|
||||
```java
|
||||
binding.categoryTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
if (tab == null) return;
|
||||
CharSequence title = tab.getText();
|
||||
currentCategory = title != null ? title.toString() : "热门";
|
||||
|
||||
// 保存选中的分类
|
||||
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
|
||||
|
||||
// 根据分类加载数据
|
||||
if ("热门".equals(currentCategory)) {
|
||||
// 加载热门作品
|
||||
refreshHotWorks();
|
||||
} else {
|
||||
// 应用其他分类筛选
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (tab == null) return;
|
||||
CharSequence title = tab.getText();
|
||||
currentCategory = title != null ? title.toString() : "热门";
|
||||
|
||||
if ("热门".equals(currentCategory)) {
|
||||
// 刷新热门作品
|
||||
refreshHotWorks();
|
||||
} else {
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 修改下拉刷新逻辑
|
||||
|
||||
```java
|
||||
binding.swipeRefresh.setOnRefreshListener(() -> {
|
||||
if ("热门".equals(currentCategory)) {
|
||||
refreshHotWorks();
|
||||
} else {
|
||||
fetchDiscoverRooms();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 修改滚动加载更多逻辑
|
||||
|
||||
```java
|
||||
binding.roomsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0) return;
|
||||
|
||||
RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
|
||||
if (!(lm instanceof GridLayoutManager)) return;
|
||||
|
||||
GridLayoutManager glm = (GridLayoutManager) lm;
|
||||
int total = glm.getItemCount();
|
||||
int lastVisible = glm.findLastVisibleItemPosition();
|
||||
|
||||
if (total <= 0) return;
|
||||
|
||||
if (lastVisible >= total - 4) {
|
||||
if ("热门".equals(currentCategory)) {
|
||||
loadMoreHotWorks();
|
||||
} else {
|
||||
// 其他分类的加载逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. WorksAdapter 修改
|
||||
|
||||
#### 添加热门标识显示
|
||||
|
||||
在 `item_works.xml` 布局中添加热门标识:
|
||||
|
||||
```xml
|
||||
<!-- 热门标识 -->
|
||||
<ImageView
|
||||
android:id="@+id/hotBadge"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_fire_24"
|
||||
android:visibility="gone"
|
||||
app:tint="#FF4500" />
|
||||
```
|
||||
|
||||
#### 修改 ViewHolder 的 bind 方法
|
||||
|
||||
```java
|
||||
public void bind(WorksResponse works, OnWorksClickListener listener) {
|
||||
if (works == null) return;
|
||||
|
||||
// ... 现有代码 ...
|
||||
|
||||
// 显示热门标识
|
||||
ImageView hotBadge = itemView.findViewById(R.id.hotBadge);
|
||||
if (hotBadge != null) {
|
||||
Boolean isHot = works.getIsHot();
|
||||
if (isHot != null && isHot) {
|
||||
hotBadge.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
hotBadge.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// ... 现有代码 ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WorksResponse 数据模型
|
||||
|
||||
确保 `WorksResponse` 类包含 `isHot` 字段:
|
||||
|
||||
```java
|
||||
public class WorksResponse {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String coverUrl;
|
||||
private String videoUrl;
|
||||
private String type;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String authorName;
|
||||
private Integer likeCount;
|
||||
private Boolean isLiked;
|
||||
private Boolean isHot; // 新增:是否热门
|
||||
private String hotTime; // 新增:设置热门的时间
|
||||
|
||||
// Getters and Setters
|
||||
public Boolean getIsHot() {
|
||||
return isHot;
|
||||
}
|
||||
|
||||
public void setIsHot(Boolean isHot) {
|
||||
this.isHot = isHot;
|
||||
}
|
||||
|
||||
public String getHotTime() {
|
||||
return hotTime;
|
||||
}
|
||||
|
||||
public void setHotTime(String hotTime) {
|
||||
this.hotTime = hotTime;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. ApiService 接口
|
||||
|
||||
已存在的接口:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 获取热门作品列表
|
||||
*/
|
||||
@GET("api/works/hot")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getHotWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### WorksResponse
|
||||
|
||||
```java
|
||||
public class WorksResponse {
|
||||
private Long id; // 作品ID
|
||||
private String title; // 作品标题
|
||||
private String coverUrl; // 封面图片URL
|
||||
private String videoUrl; // 视频URL
|
||||
private String type; // 作品类型(VIDEO/IMAGE)
|
||||
private Integer userId; // 用户ID
|
||||
private String userName; // 用户名
|
||||
private String authorName; // 作者名
|
||||
private Integer likeCount; // 点赞数
|
||||
private Boolean isLiked; // 当前用户是否已点赞
|
||||
private Boolean isHot; // 是否热门
|
||||
private String hotTime; // 设置热门的时间
|
||||
}
|
||||
```
|
||||
|
||||
### PageResponse<T>
|
||||
|
||||
```java
|
||||
public class PageResponse<T> {
|
||||
private List<T> list; // 数据列表
|
||||
private Integer total; // 总数
|
||||
private Integer page; // 当前页码
|
||||
private Integer pageSize; // 每页大小
|
||||
private Integer totalPage; // 总页数
|
||||
}
|
||||
```
|
||||
|
||||
### ApiResponse<T>
|
||||
|
||||
```java
|
||||
public class ApiResponse<T> {
|
||||
private Integer code; // 响应码(200表示成功)
|
||||
private String message; // 响应消息
|
||||
private T data; // 响应数据
|
||||
|
||||
public boolean isOk() {
|
||||
return code != null && code == 200;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### 数据库字段(后端)
|
||||
|
||||
```sql
|
||||
-- eb_works 表
|
||||
ALTER TABLE `eb_works`
|
||||
ADD COLUMN `is_hot` tinyint DEFAULT 0 COMMENT '是否热门:1-是 0-否' AFTER `status`,
|
||||
ADD COLUMN `hot_time` datetime DEFAULT NULL COMMENT '设置热门的时间' AFTER `is_hot`,
|
||||
ADD INDEX `idx_is_hot` (`is_hot`);
|
||||
```
|
||||
|
||||
### API 请求参数
|
||||
|
||||
```
|
||||
GET /api/works/hot?page=1&pageSize=20
|
||||
```
|
||||
|
||||
### API 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "精彩作品",
|
||||
"coverUrl": "https://example.com/cover.jpg",
|
||||
"videoUrl": "https://example.com/video.mp4",
|
||||
"type": "VIDEO",
|
||||
"userId": 10,
|
||||
"userName": "用户A",
|
||||
"authorName": "作者A",
|
||||
"likeCount": 100,
|
||||
"isLiked": false,
|
||||
"isHot": true,
|
||||
"hotTime": "2026-01-08 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 50,
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalPage": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 错误场景处理
|
||||
|
||||
1. **网络错误**
|
||||
- 显示错误提示:"网络错误,请重试"
|
||||
- 提供重试按钮
|
||||
- 保留已加载的数据
|
||||
|
||||
2. **API 返回错误**
|
||||
- 显示服务器返回的错误消息
|
||||
- 提供重试按钮
|
||||
|
||||
3. **空数据**
|
||||
- 显示空状态视图:"暂无热门作品"
|
||||
- 提供刷新按钮
|
||||
|
||||
4. **加载超时**
|
||||
- 设置合理的超时时间(30秒)
|
||||
- 显示超时提示
|
||||
- 提供重试选项
|
||||
|
||||
### 错误处理代码
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<WorksResponse>>> call, Throwable t) {
|
||||
isLoadingHotWorks = false;
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
|
||||
String errorMsg = "网络错误";
|
||||
if (t != null) {
|
||||
if (t.getMessage() != null && t.getMessage().contains("Unable to resolve host")) {
|
||||
errorMsg = "无法连接服务器,请检查网络";
|
||||
} else if (t.getMessage() != null && t.getMessage().contains("timeout")) {
|
||||
errorMsg = "连接超时,请重试";
|
||||
} else if (t.getMessage() != null && t.getMessage().contains("Connection refused")) {
|
||||
errorMsg = "连接被拒绝,请确保服务器已启动";
|
||||
}
|
||||
}
|
||||
|
||||
showErrorState(errorMsg);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
#### Property 1: 热门Tab仅显示作品内容
|
||||
*For any* 热门Tab中显示的数据项,该数据项应该是WorksResponse类型,不应包含Room(直播间)类型的数据
|
||||
**Validates: Requirements 2.3**
|
||||
|
||||
#### Property 2: 热门作品显示热门标识
|
||||
*For any* 在热门Tab中显示的作品,如果该作品的isHot字段为true,则作品卡片上应该显示热门图标(hotBadge可见)
|
||||
**Validates: Requirements 3.1**
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **MainActivity 单元测试**
|
||||
- 测试"热门"Tab在categoryTabs的索引为0(最左侧)**Validates: Requirements 1.1, 1.2**
|
||||
- 测试categoryTabs包含6个Tab,顺序为:热门、推荐、直播、视频、音乐、游戏 **Validates: Requirements 1.4**
|
||||
- 测试进入"发现"Tab时默认选中"热门"Tab **Validates: Requirements 1.3**
|
||||
- 测试点击"热门"Tab调用loadHotWorks方法 **Validates: Requirements 2.1**
|
||||
- 测试loadHotWorks方法调用ApiService.getHotWorks接口 **Validates: Requirements 2.1**
|
||||
- 测试接收到数据后WorksAdapter被正确更新 **Validates: Requirements 2.2**
|
||||
- 测试加载中显示骨架屏或加载动画 **Validates: Requirements 2.4**
|
||||
- 测试加载失败显示错误提示 **Validates: Requirements 2.5**
|
||||
- 测试空数据显示"暂无热门作品" **Validates: Requirements 2.6**
|
||||
- 测试下拉刷新触发refreshHotWorks方法 **Validates: Requirements 4.1**
|
||||
- 测试刷新时SwipeRefreshLayout.isRefreshing为true **Validates: Requirements 4.2**
|
||||
- 测试刷新完成后isRefreshing为false且列表更新 **Validates: Requirements 4.3**
|
||||
- 测试刷新失败显示错误提示且保留原数据 **Validates: Requirements 4.4**
|
||||
- 测试滚动到底部触发loadMoreHotWorks方法 **Validates: Requirements 5.1**
|
||||
- 测试加载更多时显示底部加载指示器 **Validates: Requirements 5.2**
|
||||
- 测试hasMoreHotWorks为false时显示"没有更多内容" **Validates: Requirements 5.3**
|
||||
- 测试加载更多失败显示错误提示 **Validates: Requirements 5.4**
|
||||
- 测试切换Tab后hotWorks列表数据保留 **Validates: Requirements 6.1**
|
||||
- 测试返回热门Tab后列表数据和滚动位置恢复 **Validates: Requirements 6.2**
|
||||
- 测试停留超过5分钟后返回触发自动刷新 **Validates: Requirements 6.3**
|
||||
|
||||
2. **WorksAdapter 单元测试**
|
||||
- 测试isHot=true时hotBadge可见 **Validates: Requirements 3.1**
|
||||
- 测试isHot=false时hotBadge不可见 **Validates: Requirements 3.1**
|
||||
- 测试点击作品触发onWorksClick回调 **Validates: Requirements 3.3**
|
||||
- 测试点击作品启动WorkDetailActivity **Validates: Requirements 3.3**
|
||||
|
||||
3. **API 接口测试**
|
||||
- 测试getHotWorks接口返回PageResponse<WorksResponse>
|
||||
- 测试page和pageSize参数正确传递
|
||||
- 测试错误响应正确处理
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **端到端测试**
|
||||
- 测试从点击"热门"Tab到显示作品列表的完整流程
|
||||
- 测试下拉刷新功能
|
||||
- 测试上拉加载更多功能
|
||||
- 测试点击作品跳转到详情页
|
||||
|
||||
2. **UI 测试**
|
||||
- 测试"热门"Tab在最左侧显示
|
||||
- 测试热门标识🔥正确显示
|
||||
- 测试加载动画正确显示
|
||||
- 测试空状态和错误状态正确显示
|
||||
|
||||
### 测试数据准备
|
||||
|
||||
```sql
|
||||
-- 准备测试数据:设置一些作品为热门
|
||||
UPDATE eb_works SET is_hot = 1, hot_time = NOW() WHERE id IN (1, 2, 3, 4, 5);
|
||||
```
|
||||
|
||||
### 测试用例
|
||||
|
||||
| 测试用例 | 输入 | 预期输出 |
|
||||
|---------|------|---------|
|
||||
| 加载热门作品 | page=1, pageSize=20 | 返回20条热门作品 |
|
||||
| 空数据 | 数据库无热门作品 | 显示"暂无热门作品" |
|
||||
| 网络错误 | 网络断开 | 显示"网络错误,请重试" |
|
||||
| 下拉刷新 | 下拉列表 | 重新加载第1页数据 |
|
||||
| 上拉加载更多 | 滚动到底部 | 加载下一页数据 |
|
||||
| 点击作品 | 点击作品卡片 | 跳转到作品详情页 |
|
||||
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
本文档定义了在Android直播应用中添加"热门"Tab功能的需求。该功能将在"发现"页面的分类标签栏(categoryTabs)中添加一个"热门"Tab,显示在"推荐"Tab的左侧(最左侧位置),展示后台管理员设置为热门的作品内容。
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Android App**: 基于Android平台的直播应用客户端
|
||||
- **topTabs**: 主页面顶部的标签栏,包含"关注"、"发现"、"附近"三个Tab
|
||||
- **categoryTabs**: "发现"Tab下方的分类标签栏,当前包含"推荐"、"直播"、"视频"、"音乐"、"游戏"五个分类
|
||||
- **热门Tab**: 新增的分类标签,显示热门作品,位于categoryTabs的最左侧("推荐"左边)
|
||||
- **热门作品**: 后台管理员通过`is_hot`字段标记为热门的作品(`is_hot=1`)
|
||||
- **作品列表**: 展示作品的RecyclerView列表,使用WorksAdapter适配器
|
||||
- **后端API**: 提供热门作品数据的服务端接口 `/api/works/hot`
|
||||
- **发现Tab**: topTabs中的"发现"标签页,包含categoryTabs分类标签栏
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1
|
||||
|
||||
**User Story:** 作为用户,我想在"发现"页面的分类标签栏看到"热门"Tab,以便快速浏览热门作品
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户打开主页面并选择"发现"Tab THEN Android App SHALL 在categoryTabs的最左侧显示"热门"Tab
|
||||
2. WHEN categoryTabs显示时 THEN Android App SHALL 确保"热门"Tab位于"推荐"Tab的左侧
|
||||
3. WHEN 用户首次进入"发现"Tab THEN Android App SHALL 默认选中"热门"Tab并显示热门作品列表
|
||||
4. WHEN categoryTabs显示时 THEN Android App SHALL 保持现有Tab(推荐、直播、视频、音乐、游戏)的位置和功能不变,仅在最左侧添加"热门"Tab
|
||||
|
||||
### Requirement 2
|
||||
|
||||
**User Story:** 作为用户,我想点击"热门"Tab查看热门作品列表,以便发现平台推荐的优质作品内容
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户点击"热门"Tab THEN Android App SHALL 调用后端API `/api/works/hot` 获取热门作品数据
|
||||
2. WHEN 后端返回热门作品数据 THEN Android App SHALL 使用WorksAdapter在RecyclerView中展示作品列表
|
||||
3. WHEN "热门"Tab显示内容 THEN Android App SHALL 仅显示作品列表,不包含直播间内容
|
||||
4. WHEN 热门作品列表加载中 THEN Android App SHALL 显示加载动画或骨架屏
|
||||
5. WHEN 热门作品列表加载失败 THEN Android App SHALL 显示错误提示并提供重试选项
|
||||
6. WHEN 热门作品列表为空 THEN Android App SHALL 显示"暂无热门作品"的空状态提示
|
||||
|
||||
### Requirement 3
|
||||
|
||||
**User Story:** 作为用户,我想在热门作品列表中看到作品的热门标识,以便识别这些是热门内容
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 作品在热门Tab中显示 THEN Android App SHALL 在作品卡片上显示"🔥"热门图标或标签
|
||||
2. WHEN 作品卡片显示热门标识 THEN Android App SHALL 确保标识位置醒目且不遮挡作品主要信息
|
||||
3. WHEN 用户点击热门作品 THEN Android App SHALL 跳转到作品详情页面
|
||||
|
||||
### Requirement 4
|
||||
|
||||
**User Story:** 作为用户,我想下拉刷新热门作品列表,以便获取最新的热门内容
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户在热门Tab中下拉列表 THEN Android App SHALL 触发刷新操作并重新请求热门作品数据
|
||||
2. WHEN 刷新操作进行中 THEN Android App SHALL 显示下拉刷新动画
|
||||
3. WHEN 刷新完成 THEN Android App SHALL 更新作品列表并隐藏刷新动画
|
||||
4. WHEN 刷新失败 THEN Android App SHALL 显示错误提示并保持当前列表数据
|
||||
|
||||
### Requirement 5
|
||||
|
||||
**User Story:** 作为用户,我想在热门作品列表滚动到底部时自动加载更多内容,以便浏览更多热门作品
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户滚动热门作品列表到底部 THEN Android App SHALL 自动请求下一页热门作品数据
|
||||
2. WHEN 加载更多数据时 THEN Android App SHALL 在列表底部显示加载指示器
|
||||
3. WHEN 没有更多热门作品时 THEN Android App SHALL 显示"没有更多内容"提示
|
||||
4. WHEN 加载更多失败 THEN Android App SHALL 显示错误提示并允许用户重试
|
||||
|
||||
### Requirement 6
|
||||
|
||||
**User Story:** 作为用户,我想在切换到其他Tab后再返回热门Tab时保持之前的浏览位置,以便继续浏览
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 用户从热门Tab切换到其他Tab THEN Android App SHALL 保存热门Tab的滚动位置和已加载数据
|
||||
2. WHEN 用户返回热门Tab THEN Android App SHALL 恢复之前的滚动位置和列表数据
|
||||
3. WHEN 用户在其他Tab停留超过5分钟后返回热门Tab THEN Android App SHALL 自动刷新热门作品列表
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
# Implementation Plan
|
||||
|
||||
- [x] 1. 修改布局文件添加"热门"Tab
|
||||
|
||||
|
||||
|
||||
|
||||
- [x] 1.1 在activity_main.xml的categoryTabs中添加"热门"TabItem,放在最左侧
|
||||
|
||||
|
||||
|
||||
|
||||
- 在categoryTabs的第一个位置添加TabItem,文本为"热门"
|
||||
- 确保"热门"Tab在"推荐"Tab之前
|
||||
- _Requirements: 1.1, 1.2, 1.4_
|
||||
|
||||
- [x] 1.2 在item_works.xml中添加热门标识ImageView
|
||||
|
||||
|
||||
|
||||
|
||||
- 添加id为hotBadge的ImageView
|
||||
- 设置火焰图标(ic_fire_24)
|
||||
- 设置橙红色tint(#FF4500)
|
||||
- 默认visibility为gone
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [x] 2. 修改WorksResponse数据模型
|
||||
|
||||
|
||||
|
||||
|
||||
- [x] 2.1 在WorksResponse类中添加isHot和hotTime字段
|
||||
|
||||
|
||||
|
||||
|
||||
- 添加Boolean类型的isHot字段
|
||||
- 添加String类型的hotTime字段
|
||||
- 添加对应的getter和setter方法
|
||||
- _Requirements: 2.2, 3.1_
|
||||
|
||||
- [x] 3. 修改WorksAdapter显示热门标识
|
||||
|
||||
|
||||
|
||||
|
||||
- [x] 3.1 在WorksViewHolder的bind方法中添加热门标识显示逻辑
|
||||
|
||||
|
||||
|
||||
|
||||
- 获取hotBadge视图
|
||||
- 根据works.getIsHot()判断是否显示
|
||||
- isHot为true时设置visibility为VISIBLE
|
||||
- isHot为false或null时设置visibility为GONE
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [ ]* 3.2 编写Property测试:热门作品显示热门标识
|
||||
- **Property 2: 热门作品显示热门标识**
|
||||
- **Validates: Requirements 3.1**
|
||||
|
||||
- [x] 4. 在MainActivity中添加热门作品数据管理
|
||||
|
||||
|
||||
|
||||
- [x] 4.1 添加热门作品相关成员变量
|
||||
|
||||
|
||||
|
||||
|
||||
- 添加hotWorks列表(List<WorksResponse>)
|
||||
- 添加currentHotWorksPage(int,初始值1)
|
||||
- 添加isLoadingHotWorks(boolean,初始值false)
|
||||
- 添加hasMoreHotWorks(boolean,初始值true)
|
||||
- _Requirements: 2.1, 5.1_
|
||||
|
||||
- [x] 4.2 实现loadHotWorks方法
|
||||
|
||||
|
||||
|
||||
|
||||
- 检查isLoadingHotWorks,避免重复请求
|
||||
- 调用ApiService.getHotWorks(page, 20)
|
||||
- 处理成功响应:更新hotWorks列表,更新hasMoreHotWorks标志
|
||||
- 处理失败响应:显示错误提示
|
||||
- 更新isLoadingHotWorks状态
|
||||
- _Requirements: 2.1, 2.2, 2.4, 2.5_
|
||||
|
||||
- [x] 4.3 实现refreshHotWorks方法
|
||||
|
||||
|
||||
- 重置currentHotWorksPage为1
|
||||
- 重置hasMoreHotWorks为true
|
||||
- 调用loadHotWorks(1)
|
||||
- _Requirements: 4.1, 4.3_
|
||||
|
||||
- [x] 4.4 实现loadMoreHotWorks方法
|
||||
|
||||
- 检查isLoadingHotWorks和hasMoreHotWorks
|
||||
- 增加currentHotWorksPage
|
||||
- 调用loadHotWorks(currentHotWorksPage)
|
||||
- _Requirements: 5.1, 5.3_
|
||||
|
||||
- [x] 4.5 实现updateHotWorksUI方法
|
||||
|
||||
- 检查hotWorks是否为空
|
||||
- 为空时显示空状态视图
|
||||
- 不为空时转换为FeedItem列表并更新adapter
|
||||
- _Requirements: 2.2, 2.6_
|
||||
|
||||
- [ ]* 4.6 编写Property测试:热门Tab仅显示作品内容
|
||||
- **Property 1: 热门Tab仅显示作品内容**
|
||||
- **Validates: Requirements 2.3**
|
||||
|
||||
- [x] 5. 修改categoryTabs的Tab选择监听器
|
||||
|
||||
|
||||
- [x] 5.1 修改onTabSelected方法处理"热门"Tab
|
||||
|
||||
- 获取选中Tab的文本
|
||||
- 判断是否为"热门"
|
||||
- 如果是"热门",调用refreshHotWorks()
|
||||
- 如果是其他分类,调用applyCategoryFilterWithAnimation()
|
||||
- _Requirements: 1.3, 2.1, 4.1_
|
||||
|
||||
- [x] 5.2 修改onTabReselected方法处理"热门"Tab
|
||||
|
||||
- 判断是否为"热门"
|
||||
- 如果是"热门",调用refreshHotWorks()刷新数据
|
||||
- _Requirements: 4.1_
|
||||
|
||||
- [x] 6. 修改下拉刷新逻辑
|
||||
|
||||
|
||||
- [x] 6.1 在SwipeRefreshLayout的OnRefreshListener中添加"热门"Tab判断
|
||||
|
||||
|
||||
- 检查currentCategory是否为"热门"
|
||||
- 如果是"热门",调用refreshHotWorks()
|
||||
- 如果不是,调用fetchDiscoverRooms()
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 7. 修改滚动加载更多逻辑
|
||||
|
||||
|
||||
- [x] 7.1 在RecyclerView的OnScrollListener中添加"热门"Tab判断
|
||||
|
||||
|
||||
- 检查是否滚动到底部(lastVisible >= total - 4)
|
||||
- 检查currentCategory是否为"热门"
|
||||
- 如果是"热门",调用loadMoreHotWorks()
|
||||
- _Requirements: 5.1, 5.2_
|
||||
|
||||
- [-] 8. 实现Tab切换时的状态保存和恢复
|
||||
|
||||
- [x] 8.1 添加Tab切换时间记录
|
||||
|
||||
|
||||
- 添加lastHotTabLeaveTime成员变量(long)
|
||||
- 在切换离开"热门"Tab时记录当前时间
|
||||
- _Requirements: 6.1, 6.3_
|
||||
|
||||
- [ ] 8.2 实现返回"热门"Tab时的状态恢复
|
||||
|
||||
|
||||
- 检查是否有已加载的数据
|
||||
- 如果有数据且停留时间小于5分钟,恢复数据和滚动位置
|
||||
- 如果停留时间超过5分钟,调用refreshHotWorks()
|
||||
- _Requirements: 6.2, 6.3_
|
||||
|
||||
- [x] 9. 添加错误处理和空状态显示
|
||||
|
||||
|
||||
- [x] 9.1 实现showErrorState方法
|
||||
|
||||
- 显示错误提示视图
|
||||
- 提供重试按钮
|
||||
- 重试按钮点击时调用refreshHotWorks()
|
||||
- _Requirements: 2.5, 4.4, 5.4_
|
||||
|
||||
- [x] 9.2 实现showEmptyState方法
|
||||
|
||||
- 显示空状态视图
|
||||
- 显示"暂无热门作品"文本
|
||||
- 提供刷新按钮
|
||||
- _Requirements: 2.6_
|
||||
|
||||
- [x] 10. 设置默认选中"热门"Tab
|
||||
|
||||
|
||||
|
||||
- [x] 10.1 在showDiscoverTab方法中设置默认选中
|
||||
|
||||
|
||||
- 在categoryTabs初始化完成后
|
||||
- 获取索引为0的Tab("热门"Tab)
|
||||
- 调用tab.select()选中该Tab
|
||||
- 触发loadHotWorks加载数据
|
||||
- _Requirements: 1.3_
|
||||
|
||||
- [ ]* 11. 编写单元测试
|
||||
- [ ]* 11.1 测试"热门"Tab位置和顺序
|
||||
- 测试categoryTabs包含6个Tab
|
||||
- 测试"热门"Tab在索引0位置
|
||||
- 测试"推荐"Tab在索引1位置
|
||||
- _Requirements: 1.1, 1.2, 1.4_
|
||||
|
||||
- [ ]* 11.2 测试热门作品加载逻辑
|
||||
- 测试loadHotWorks调用正确的API
|
||||
- 测试refreshHotWorks重置页码
|
||||
- 测试loadMoreHotWorks增加页码
|
||||
- 测试空数据显示空状态
|
||||
- 测试错误显示错误提示
|
||||
- _Requirements: 2.1, 2.4, 2.5, 2.6, 4.1, 5.1_
|
||||
|
||||
- [ ]* 11.3 测试WorksAdapter热门标识显示
|
||||
- 测试isHot=true时hotBadge可见
|
||||
- 测试isHot=false时hotBadge不可见
|
||||
- _Requirements: 3.1_
|
||||
|
||||
- [ ]* 11.4 测试下拉刷新和加载更多
|
||||
- 测试下拉刷新触发refreshHotWorks
|
||||
- 测试滚动到底部触发loadMoreHotWorks
|
||||
- 测试刷新动画显示和隐藏
|
||||
- _Requirements: 4.1, 4.2, 4.3, 5.1, 5.2_
|
||||
|
||||
- [ ]* 11.5 测试Tab切换状态保存
|
||||
- 测试切换Tab后数据保留
|
||||
- 测试返回Tab后数据恢复
|
||||
- 测试超过5分钟自动刷新
|
||||
- _Requirements: 6.1, 6.2, 6.3_
|
||||
|
||||
- [ ] 12. Checkpoint - 确保所有测试通过
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
|
|
@ -118,37 +118,3 @@ export function UpdateWorksStatus(pram) {
|
|||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/取消热门
|
||||
* @param pram
|
||||
* @constructor
|
||||
*/
|
||||
export function ToggleWorksHot(pram) {
|
||||
const data = {
|
||||
id: pram.id,
|
||||
isHot: pram.isHot,
|
||||
};
|
||||
return request({
|
||||
url: '/admin/works/toggleHot',
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门作品列表
|
||||
* @param pram
|
||||
* @constructor
|
||||
*/
|
||||
export function ListHotWorks(pram) {
|
||||
const data = {
|
||||
page: pram.page,
|
||||
limit: pram.limit,
|
||||
};
|
||||
return request({
|
||||
url: '/admin/works/hotList',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,16 +69,8 @@
|
|||
<el-tag v-else type="danger">已删除</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isHot" label="热门" min-width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.isHot === 1" type="danger" effect="dark">
|
||||
<i class="el-icon-star-on"></i> 热门
|
||||
</el-tag>
|
||||
<span v-else style="color: #999">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<router-link :to="{ path: '/contentManage/article/edit/' + scope.row.id }">
|
||||
<a v-hasPermi="['admin:works:info']">编辑</a>
|
||||
|
|
@ -87,14 +79,6 @@
|
|||
<a @click="handleUpdateStatus(scope.row)" v-hasPermi="['admin:works:update']">
|
||||
{{ scope.row.status === 1 ? '下架' : '上架' }}
|
||||
</a>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<a
|
||||
@click="handleToggleHot(scope.row)"
|
||||
v-hasPermi="['admin:works:update']"
|
||||
:style="{ color: scope.row.isHot === 1 ? '#F56C6C' : '#409EFF' }"
|
||||
>
|
||||
{{ scope.row.isHot === 1 ? '取消热门' : '设为热门' }}
|
||||
</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -180,18 +164,6 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
handleToggleHot(rowData) {
|
||||
const isHot = rowData.isHot === 1 ? false : true;
|
||||
const action = isHot ? '设为热门' : '取消热门';
|
||||
this.$modalSure(`确定要${action}该作品吗?`).then(() => {
|
||||
worksApi.ToggleWorksHot({ id: rowData.id, isHot: isHot }).then(() => {
|
||||
this.$message.success(`${action}成功`);
|
||||
this.handlerGetListData(this.listPram);
|
||||
}).catch(err => {
|
||||
this.$message.error(err.message || `${action}失败`);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.listPram.limit = val;
|
||||
this.handlerGetListData(this.listPram);
|
||||
|
|
|
|||
|
|
@ -148,43 +148,4 @@ public class WorksController {
|
|||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/取消热门
|
||||
* @param id Long 作品ID
|
||||
* @param isHot Boolean 是否热门:true-设置热门 false-取消热门
|
||||
*/
|
||||
// @PreAuthorize("hasAuthority('admin:works:update')") // 临时注释,等待添加权限
|
||||
@ApiOperation(value = "设置/取消热门")
|
||||
@RequestMapping(value = "/toggleHot", method = RequestMethod.POST)
|
||||
public CommonResult<String> toggleHot(@RequestParam Long id, @RequestParam Boolean isHot) {
|
||||
try {
|
||||
worksService.toggleHot(id, isHot);
|
||||
return CommonResult.success(isHot ? "设置热门成功" : "取消热门成功");
|
||||
} catch (Exception e) {
|
||||
log.error("设置热门失败", e);
|
||||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门作品列表
|
||||
* @param pageParamRequest 分页参数
|
||||
*/
|
||||
// @PreAuthorize("hasAuthority('admin:works:list')") // 临时注释,等待添加权限
|
||||
@ApiOperation(value = "热门作品列表")
|
||||
@RequestMapping(value = "/hotList", method = RequestMethod.GET)
|
||||
public CommonResult<CommonPage<WorksResponse>> getHotList(@Validated PageParamRequest pageParamRequest) {
|
||||
try {
|
||||
CommonPage<WorksResponse> result = worksService.getHotWorks(
|
||||
pageParamRequest.getPage(),
|
||||
pageParamRequest.getLimit(),
|
||||
null
|
||||
);
|
||||
return CommonResult.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门作品列表失败", e);
|
||||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,15 +105,6 @@ public class Works implements Serializable {
|
|||
@ApiModelProperty(value = "状态:1-正常 0-下架")
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "is_hot", nullable = false, columnDefinition = "TINYINT DEFAULT 0 COMMENT '是否热门:1-是 0-否'")
|
||||
@ApiModelProperty(value = "是否热门:1-是 0-否")
|
||||
private Integer isHot = 0;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "hot_time", columnDefinition = "DATETIME COMMENT '设置热门的时间'")
|
||||
@ApiModelProperty(value = "设置热门的时间")
|
||||
private Date hotTime;
|
||||
|
||||
@TableLogic
|
||||
@Column(name = "is_deleted", nullable = false, columnDefinition = "TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除 1-已删除'")
|
||||
@ApiModelProperty(value = "逻辑删除:0-未删除 1-已删除")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import io.swagger.annotations.ApiModelProperty;
|
|||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
|
|
@ -18,6 +19,7 @@ public class WorksRequest {
|
|||
private Long id;
|
||||
|
||||
@ApiModelProperty(value = "作品标题", required = true)
|
||||
@NotBlank(message = "作品标题不能为空")
|
||||
@Size(max = 200, message = "作品标题最多200个字符")
|
||||
private String title;
|
||||
|
||||
|
|
|
|||
|
|
@ -73,12 +73,6 @@ public class WorksResponse {
|
|||
@ApiModelProperty(value = "状态:1-正常 0-下架")
|
||||
private Integer status;
|
||||
|
||||
@ApiModelProperty(value = "是否热门:1-是 0-否")
|
||||
private Integer isHot;
|
||||
|
||||
@ApiModelProperty(value = "设置热门的时间")
|
||||
private Date hotTime;
|
||||
|
||||
@ApiModelProperty(value = "是否已点赞")
|
||||
private Boolean isLiked = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -71,26 +71,6 @@ public class WorksController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOperation(value = "获取附近作品列表")
|
||||
@GetMapping("/nearby")
|
||||
public CommonResult<CommonPage<WorksResponse>> getNearbyWorks(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "limit", defaultValue = "20") Integer limit) {
|
||||
|
||||
Integer userId = userService.getUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.unauthorized("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
CommonPage<WorksResponse> result = worksService.getNearbyWorks(page, limit, userId);
|
||||
return CommonResult.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("获取附近作品列表失败", e);
|
||||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑作品(需要登录)
|
||||
*/
|
||||
|
|
@ -350,25 +330,4 @@ public class WorksController {
|
|||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门作品列表(不需要登录)
|
||||
*/
|
||||
@ApiOperation(value = "获取热门作品列表")
|
||||
@GetMapping("/hot")
|
||||
public CommonResult<CommonPage<WorksResponse>> getHotWorks(
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
|
||||
try {
|
||||
// 获取当前登录用户ID(如果已登录)
|
||||
Integer userId = userService.getUserId();
|
||||
|
||||
CommonPage<WorksResponse> result = worksService.getHotWorks(page, pageSize, userId);
|
||||
return CommonResult.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门作品列表失败", e);
|
||||
return CommonResult.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,15 +71,6 @@ public interface WorksService extends IService<Works> {
|
|||
*/
|
||||
CommonPage<WorksResponse> getUserWorks(Integer userId, Integer page, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 获取附近作品列表(按距离从近到远排序)
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @param userId 当前用户ID
|
||||
* @return 附近作品列表
|
||||
*/
|
||||
CommonPage<WorksResponse> getNearbyWorks(Integer page, Integer limit, Integer userId);
|
||||
|
||||
/**
|
||||
* 增加浏览次数
|
||||
* @param worksId 作品ID
|
||||
|
|
@ -91,21 +82,4 @@ public interface WorksService extends IService<Works> {
|
|||
* @param worksId 作品ID
|
||||
*/
|
||||
void increaseShareCount(Long worksId);
|
||||
|
||||
/**
|
||||
* 设置/取消热门
|
||||
* @param worksId 作品ID
|
||||
* @param isHot 是否热门:true-设置热门 false-取消热门
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean toggleHot(Long worksId, Boolean isHot);
|
||||
|
||||
/**
|
||||
* 获取热门作品列表
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param userId 当前用户ID(可为空)
|
||||
* @return 热门作品列表
|
||||
*/
|
||||
CommonPage<WorksResponse> getHotWorks(Integer page, Integer pageSize, Integer userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,20 +320,6 @@ public class UploadServiceImpl implements UploadService {
|
|||
systemAttachmentService.save(systemAttachment);
|
||||
return resultFile;
|
||||
}
|
||||
|
||||
File parentDir = file.getParentFile();
|
||||
if (!parentDir.exists()) {
|
||||
logger.info("父目录不存在,开始创建: {}", parentDir.getAbsolutePath());
|
||||
boolean created = parentDir.mkdirs();
|
||||
if (!created) {
|
||||
logger.error("创建目录失败: {}", parentDir.getAbsolutePath());
|
||||
throw new CrmebException("无法创建上传目录: " + parentDir.getAbsolutePath());
|
||||
}
|
||||
logger.info("目录创建成功!");
|
||||
} else {
|
||||
logger.info("父目录已存在: {}", parentDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
CloudVo cloudVo = new CloudVo();
|
||||
// 判断是否保存本地
|
||||
String fileIsSave = systemConfigService.getValueByKeyException(SysConfigConstants.CONFIG_FILE_IS_SAVE);
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
|
@ -62,8 +58,11 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
|
||||
// 验证作品类型和媒体资源
|
||||
if ("IMAGE".equalsIgnoreCase(request.getType())) {
|
||||
// 图片作品允许只上传封面(图片列表可为空),但如果上传了图片,最多9张
|
||||
if (request.getImageUrls() != null && request.getImageUrls().size() > 9) {
|
||||
// 图片作品必须有图片列表
|
||||
if (request.getImageUrls() == null || request.getImageUrls().isEmpty()) {
|
||||
throw new CrmebException("图片作品必须上传至少一张图片");
|
||||
}
|
||||
if (request.getImageUrls().size() > 9) {
|
||||
throw new CrmebException("图片作品最多只能上传9张图片");
|
||||
}
|
||||
} else if ("VIDEO".equalsIgnoreCase(request.getType())) {
|
||||
|
|
@ -86,8 +85,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
// 创建作品对象
|
||||
Works works = new Works();
|
||||
works.setUid(userId);
|
||||
// title 字段数据库 NOT NULL,允许为空时写入空字符串
|
||||
works.setTitle(StrUtil.blankToDefault(request.getTitle(), ""));
|
||||
works.setTitle(request.getTitle());
|
||||
works.setDescription(request.getDescription());
|
||||
works.setType(request.getType().toUpperCase()); // 统一转为大写
|
||||
works.setCoverImage(request.getCoverUrl());
|
||||
|
|
@ -197,8 +195,7 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
|
||||
// 更新作品信息
|
||||
if (request.getTitle() != null) {
|
||||
// title 字段数据库 NOT NULL,允许为空时写入空字符串
|
||||
works.setTitle(StrUtil.blankToDefault(request.getTitle(), ""));
|
||||
works.setTitle(request.getTitle());
|
||||
}
|
||||
if (request.getDescription() != null) {
|
||||
works.setDescription(request.getDescription());
|
||||
|
|
@ -220,10 +217,6 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
|
||||
// 处理图片列表
|
||||
if (request.getImageUrls() != null) {
|
||||
// 图片列表允许为空(仅封面发布)
|
||||
if (!request.getImageUrls().isEmpty() && request.getImageUrls().size() > 9) {
|
||||
throw new CrmebException("图片作品最多只能上传9张图片");
|
||||
}
|
||||
if (request.getImageUrls().isEmpty()) {
|
||||
works.setImages(null);
|
||||
} else {
|
||||
|
|
@ -461,117 +454,6 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
return searchWorks(request, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonPage<WorksResponse> getNearbyWorks(Integer page, Integer limit, Integer userId) {
|
||||
if (userId == null) {
|
||||
throw new CrmebException("请先登录");
|
||||
}
|
||||
if (page == null || page < 1) page = 1;
|
||||
if (limit == null || limit < 1) limit = 20;
|
||||
|
||||
User currentUser = userService.getById(userId);
|
||||
if (currentUser == null) {
|
||||
throw new CrmebException("用户不存在");
|
||||
}
|
||||
if (currentUser.getLatitude() == null || currentUser.getLongitude() == null) {
|
||||
throw new CrmebException("请先开启定位");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
|
||||
userWrapper.eq(User::getStatus, true)
|
||||
.ne(User::getUid, userId)
|
||||
.isNotNull(User::getLatitude)
|
||||
.isNotNull(User::getLongitude)
|
||||
.last("LIMIT 2000");
|
||||
List<User> users = userService.list(userWrapper);
|
||||
|
||||
Map<Integer, Double> userDistanceMap = new HashMap<>();
|
||||
for (User u : users) {
|
||||
double d = calculateDistanceKm(
|
||||
currentUser.getLatitude().doubleValue(),
|
||||
currentUser.getLongitude().doubleValue(),
|
||||
u.getLatitude().doubleValue(),
|
||||
u.getLongitude().doubleValue());
|
||||
userDistanceMap.put(u.getUid(), d);
|
||||
}
|
||||
|
||||
List<Integer> sortedUserIds = users.stream()
|
||||
.map(User::getUid)
|
||||
.filter(uid -> uid != null)
|
||||
.sorted(Comparator.comparingDouble(uid -> userDistanceMap.getOrDefault(uid, Double.MAX_VALUE)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (sortedUserIds.isEmpty()) {
|
||||
CommonPage<WorksResponse> empty = new CommonPage<>();
|
||||
empty.setPage(page);
|
||||
empty.setLimit(limit);
|
||||
empty.setTotal(0L);
|
||||
empty.setTotalPage(0);
|
||||
empty.setList(new ArrayList<>());
|
||||
return empty;
|
||||
}
|
||||
|
||||
int userPickCount = Math.min(sortedUserIds.size(), Math.max(limit * 50, 500));
|
||||
List<Integer> candidateUserIds = sortedUserIds.subList(0, userPickCount);
|
||||
|
||||
int worksPickCount = Math.max(limit * 200, 2000);
|
||||
Page<Works> worksPage = new Page<>(1, worksPickCount);
|
||||
LambdaQueryWrapper<Works> worksWrapper = new LambdaQueryWrapper<>();
|
||||
worksWrapper.eq(Works::getIsDeleted, 0)
|
||||
.eq(Works::getStatus, 1)
|
||||
.ne(Works::getUid, userId)
|
||||
.in(Works::getUid, candidateUserIds)
|
||||
.orderByDesc(Works::getCreateTime);
|
||||
Page<Works> candidateWorksPage = page(worksPage, worksWrapper);
|
||||
List<Works> candidateWorks = candidateWorksPage.getRecords() != null ? candidateWorksPage.getRecords() : Collections.emptyList();
|
||||
|
||||
List<Works> sortedWorks = new ArrayList<>(candidateWorks);
|
||||
sortedWorks.sort((a, b) -> {
|
||||
Double da = userDistanceMap.getOrDefault(a.getUid(), Double.MAX_VALUE);
|
||||
Double db = userDistanceMap.getOrDefault(b.getUid(), Double.MAX_VALUE);
|
||||
int c = Double.compare(da, db);
|
||||
if (c != 0) return c;
|
||||
if (a.getCreateTime() == null && b.getCreateTime() == null) return 0;
|
||||
if (a.getCreateTime() == null) return 1;
|
||||
if (b.getCreateTime() == null) return -1;
|
||||
return b.getCreateTime().compareTo(a.getCreateTime());
|
||||
});
|
||||
|
||||
long total = sortedWorks.size();
|
||||
int start = (page - 1) * limit;
|
||||
int end = (int) Math.min(total, start + limit);
|
||||
List<WorksResponse> list;
|
||||
if (start >= total) {
|
||||
list = new ArrayList<>();
|
||||
} else {
|
||||
list = sortedWorks.subList(start, end)
|
||||
.stream()
|
||||
.map(w -> convertToResponse(w, userId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
CommonPage<WorksResponse> result = new CommonPage<>();
|
||||
result.setPage(page);
|
||||
result.setLimit(limit);
|
||||
result.setTotal(total);
|
||||
result.setTotalPage((int) Math.ceil(total * 1.0 / limit));
|
||||
result.setList(list);
|
||||
return result;
|
||||
}
|
||||
|
||||
private double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
final double EARTH_RADIUS = 6371.0;
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double deltaLat = Math.toRadians(lat2 - lat1);
|
||||
double deltaLon = Math.toRadians(lon2 - lon1);
|
||||
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2)
|
||||
+ Math.cos(lat1Rad) * Math.cos(lat2Rad)
|
||||
* Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return EARTH_RADIUS * c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void increaseViewCount(Long worksId) {
|
||||
try {
|
||||
|
|
@ -623,8 +505,6 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
response.setCommentCount(works.getCommentCount());
|
||||
response.setShareCount(works.getShareCount());
|
||||
response.setStatus(works.getStatus());
|
||||
response.setIsHot(works.getIsHot());
|
||||
response.setHotTime(works.getHotTime());
|
||||
response.setCreateTime(works.getCreateTime());
|
||||
response.setUpdateTime(works.getUpdateTime());
|
||||
|
||||
|
|
@ -656,61 +536,4 @@ public class WorksServiceImpl extends ServiceImpl<WorksDao, Works> implements Wo
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean toggleHot(Long worksId, Boolean isHot) {
|
||||
// 查询作品
|
||||
Works works = getById(worksId);
|
||||
if (works == null || works.getIsDeleted() == 1) {
|
||||
throw new CrmebException("作品不存在");
|
||||
}
|
||||
|
||||
// 更新热门状态
|
||||
LambdaUpdateWrapper<Works> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(Works::getId, worksId)
|
||||
.set(Works::getIsHot, isHot ? 1 : 0)
|
||||
.set(Works::getHotTime, isHot ? new java.util.Date() : null);
|
||||
|
||||
boolean updated = update(updateWrapper);
|
||||
if (!updated) {
|
||||
throw new CrmebException("更新热门状态失败");
|
||||
}
|
||||
|
||||
log.info("{}热门成功,作品ID:{}", isHot ? "设置" : "取消", worksId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonPage<WorksResponse> getHotWorks(Integer page, Integer pageSize, Integer userId) {
|
||||
log.info("=== 获取热门作品列表 === page={}, pageSize={}, userId={}", page, pageSize, userId);
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Works> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(Works::getIsDeleted, 0)
|
||||
.eq(Works::getStatus, 1) // 只查询正常状态的作品
|
||||
.eq(Works::getIsHot, 1) // 只查询热门作品
|
||||
.orderByDesc(Works::getHotTime); // 按设置热门的时间倒序
|
||||
|
||||
// 分页查询
|
||||
Page<Works> worksPage = new Page<>(page, pageSize);
|
||||
worksPage = page(worksPage, queryWrapper);
|
||||
|
||||
log.info("查询到 {} 条热门作品记录", worksPage.getRecords().size());
|
||||
|
||||
// 转换为响应对象
|
||||
List<WorksResponse> responseList = worksPage.getRecords().stream()
|
||||
.map(works -> convertToResponse(works, userId))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建分页结果
|
||||
CommonPage<WorksResponse> result = new CommonPage<>();
|
||||
result.setList(responseList);
|
||||
result.setTotal(worksPage.getTotal());
|
||||
result.setPage((int) worksPage.getCurrent());
|
||||
result.setLimit((int) worksPage.getSize());
|
||||
result.setTotalPage((int) worksPage.getPages());
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
-- 添加作品热门相关字段
|
||||
-- 执行时间:2026-01-08
|
||||
|
||||
USE `zhibo`;
|
||||
|
||||
-- 检查字段是否已存在,如果不存在则添加
|
||||
-- 添加 is_hot 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'zhibo'
|
||||
AND TABLE_NAME = 'eb_works'
|
||||
AND COLUMN_NAME = 'is_hot';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `eb_works` ADD COLUMN `is_hot` tinyint DEFAULT 0 COMMENT ''是否热门:1-是 0-否'' AFTER `status`',
|
||||
'SELECT ''字段 is_hot 已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加 hot_time 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'zhibo'
|
||||
AND TABLE_NAME = 'eb_works'
|
||||
AND COLUMN_NAME = 'hot_time';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `eb_works` ADD COLUMN `hot_time` datetime DEFAULT NULL COMMENT ''设置热门的时间'' AFTER `is_hot`',
|
||||
'SELECT ''字段 hot_time 已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加索引(如果不存在)
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'zhibo'
|
||||
AND TABLE_NAME = 'eb_works'
|
||||
AND INDEX_NAME = 'idx_is_hot';
|
||||
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `eb_works` ADD INDEX `idx_is_hot` (`is_hot`)',
|
||||
'SELECT ''索引 idx_is_hot 已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 查看最终结果
|
||||
SHOW COLUMNS FROM `eb_works` LIKE '%hot%';
|
||||
SELECT '作品热门字段添加完成!' AS result;
|
||||
|
|
@ -214,11 +214,6 @@
|
|||
android:name="com.example.livestreaming.ConversationActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.BurnImageViewerActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.PlayerActivity"
|
||||
android:exported="false" />
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.BurnViewResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class BurnImageViewerActivity extends AppCompatActivity {
|
||||
|
||||
public static final String EXTRA_MESSAGE_ID = "extra_message_id";
|
||||
|
||||
private ImageView imageView;
|
||||
private TextView countdownText;
|
||||
private TextView tipText;
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private Runnable countdownRunnable;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
setContentView(R.layout.activity_burn_image_viewer);
|
||||
|
||||
imageView = findViewById(R.id.imageView);
|
||||
countdownText = findViewById(R.id.countdownText);
|
||||
tipText = findViewById(R.id.tipText);
|
||||
|
||||
View rootLayout = findViewById(R.id.rootLayout);
|
||||
if (rootLayout != null) {
|
||||
rootLayout.setOnClickListener(v -> finish());
|
||||
}
|
||||
|
||||
long messageId = getIntent().getLongExtra(EXTRA_MESSAGE_ID, 0L);
|
||||
if (messageId <= 0L) {
|
||||
showTipAndFinish("消息ID无效");
|
||||
return;
|
||||
}
|
||||
|
||||
ApiService api = ApiClient.getService(this);
|
||||
if (api == null) {
|
||||
showTipAndFinish("请先登录");
|
||||
return;
|
||||
}
|
||||
|
||||
api.viewBurnImage(messageId).enqueue(new Callback<ApiResponse<BurnViewResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<BurnViewResponse>> call, Response<ApiResponse<BurnViewResponse>> response) {
|
||||
if (!response.isSuccessful() || response.body() == null || !response.body().isOk()) {
|
||||
String msg = response.body() != null ? response.body().getMessage() : "查看失败";
|
||||
showTipAndFinish(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
BurnViewResponse data = response.body().getData();
|
||||
if (data == null) {
|
||||
showTipAndFinish("查看失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.getBurned() != null && data.getBurned()) {
|
||||
showTipAndFinish("已销毁");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = data.getMediaUrl();
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
showTipAndFinish("不可查看");
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageView != null) {
|
||||
Glide.with(BurnImageViewerActivity.this)
|
||||
.load(url)
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
Long burnAt = data.getBurnAt();
|
||||
Integer burnSeconds = data.getBurnSeconds();
|
||||
startCountdown(burnAt, burnSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<BurnViewResponse>> call, Throwable t) {
|
||||
showTipAndFinish("网络错误");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startCountdown(Long burnAt, Integer burnSeconds) {
|
||||
long now = System.currentTimeMillis();
|
||||
long endAt = burnAt != null ? burnAt : (burnSeconds != null ? now + burnSeconds * 1000L : now);
|
||||
|
||||
if (countdownText != null) {
|
||||
countdownText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (countdownRunnable != null) {
|
||||
handler.removeCallbacks(countdownRunnable);
|
||||
}
|
||||
|
||||
countdownRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long leftMs = endAt - System.currentTimeMillis();
|
||||
if (leftMs <= 0L) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
long sec = (leftMs + 999L) / 1000L;
|
||||
if (countdownText != null) {
|
||||
countdownText.setText(sec + "s");
|
||||
}
|
||||
handler.postDelayed(this, 200L);
|
||||
}
|
||||
};
|
||||
|
||||
handler.post(countdownRunnable);
|
||||
}
|
||||
|
||||
private void showTipAndFinish(String tip) {
|
||||
if (tipText != null) {
|
||||
tipText.setVisibility(View.VISIBLE);
|
||||
tipText.setText(tip);
|
||||
}
|
||||
handler.postDelayed(this::finish, 800L);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (countdownRunnable != null) {
|
||||
handler.removeCallbacks(countdownRunnable);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,7 @@ public class ChatMessage {
|
|||
public enum MessageType {
|
||||
TEXT, // 文本消息
|
||||
IMAGE, // 图片消息
|
||||
VOICE, // 语音消息
|
||||
BURN_IMAGE
|
||||
VOICE // 语音消息
|
||||
}
|
||||
|
||||
private String messageId;
|
||||
|
|
@ -37,12 +36,7 @@ public class ChatMessage {
|
|||
private int voiceDuration; // 语音时长(秒)
|
||||
private int imageWidth; // 图片宽度
|
||||
private int imageHeight; // 图片高度
|
||||
|
||||
private Integer burnSeconds;
|
||||
private Long viewedAt;
|
||||
private Long burnAt;
|
||||
private Boolean burned;
|
||||
|
||||
|
||||
// 表情回应相关字段
|
||||
private List<MessageReaction> reactions; // 表情回应列表
|
||||
|
||||
|
|
@ -129,16 +123,6 @@ public class ChatMessage {
|
|||
return msg;
|
||||
}
|
||||
|
||||
public static ChatMessage createBurnImageMessage(String username, String localPath, Integer burnSeconds) {
|
||||
ChatMessage msg = new ChatMessage(username, "");
|
||||
msg.messageType = MessageType.BURN_IMAGE;
|
||||
msg.localMediaPath = localPath;
|
||||
msg.burnSeconds = burnSeconds;
|
||||
msg.burned = false;
|
||||
msg.status = "我".equals(username) ? MessageStatus.SENDING : MessageStatus.SENT;
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
|
|
@ -260,38 +244,6 @@ public class ChatMessage {
|
|||
this.imageHeight = imageHeight;
|
||||
}
|
||||
|
||||
public Integer getBurnSeconds() {
|
||||
return burnSeconds;
|
||||
}
|
||||
|
||||
public void setBurnSeconds(Integer burnSeconds) {
|
||||
this.burnSeconds = burnSeconds;
|
||||
}
|
||||
|
||||
public Long getViewedAt() {
|
||||
return viewedAt;
|
||||
}
|
||||
|
||||
public void setViewedAt(Long viewedAt) {
|
||||
this.viewedAt = viewedAt;
|
||||
}
|
||||
|
||||
public Long getBurnAt() {
|
||||
return burnAt;
|
||||
}
|
||||
|
||||
public void setBurnAt(Long burnAt) {
|
||||
this.burnAt = burnAt;
|
||||
}
|
||||
|
||||
public Boolean getBurned() {
|
||||
return burned;
|
||||
}
|
||||
|
||||
public void setBurned(Boolean burned) {
|
||||
this.burned = burned;
|
||||
}
|
||||
|
||||
public List<MessageReaction> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ import android.content.ClipData;
|
|||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
|
@ -21,20 +19,13 @@ import androidx.appcompat.widget.PopupMenu;
|
|||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ApiService;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.example.livestreaming.net.FileUploadResponse;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -46,7 +37,6 @@ import okhttp3.OkHttpClient;
|
|||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.MultipartBody;
|
||||
|
||||
public class ConversationActivity extends AppCompatActivity {
|
||||
|
||||
|
|
@ -76,9 +66,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
private String otherUserName;
|
||||
private String otherUserAvatarUrl;
|
||||
|
||||
private static final int REQ_PICK_BURN_IMAGE = 4101;
|
||||
private Integer pendingBurnSeconds;
|
||||
|
||||
public static void start(Context context, String conversationId, String title) {
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(EXTRA_CONVERSATION_ID, conversationId);
|
||||
|
|
@ -208,53 +195,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
showMessageMenu(message, position, view);
|
||||
});
|
||||
|
||||
adapter.setOnImageClickListener((message, imageView) -> {
|
||||
if (message == null) return;
|
||||
|
||||
if (message.getMessageType() == ChatMessage.MessageType.BURN_IMAGE) {
|
||||
long mid = 0L;
|
||||
try {
|
||||
mid = Long.parseLong(message.getMessageId());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (mid <= 0L) {
|
||||
Snackbar.make(binding.getRoot(), "消息ID无效", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Intent it = new Intent(ConversationActivity.this, BurnImageViewerActivity.class);
|
||||
it.putExtra(BurnImageViewerActivity.EXTRA_MESSAGE_ID, mid);
|
||||
startActivity(it);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.getMessageType() == ChatMessage.MessageType.IMAGE) {
|
||||
String url = message.getMediaUrl();
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
AvatarViewerDialog.create(ConversationActivity.this)
|
||||
.setAvatarUrl(url)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
String localPath = message.getLocalMediaPath();
|
||||
if (!TextUtils.isEmpty(localPath)) {
|
||||
try {
|
||||
android.net.Uri uri;
|
||||
if (localPath.startsWith("content://") || localPath.startsWith("file://")) {
|
||||
uri = android.net.Uri.parse(localPath);
|
||||
} else {
|
||||
uri = android.net.Uri.fromFile(new File(localPath));
|
||||
}
|
||||
AvatarViewerDialog.create(ConversationActivity.this)
|
||||
.setAvatarUri(uri)
|
||||
.show();
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setStackFromEnd(false);
|
||||
binding.messagesRecyclerView.setLayoutManager(layoutManager);
|
||||
|
|
@ -512,33 +452,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
String avatarUrl = item.optString("avatarUrl", "");
|
||||
boolean isSystem = item.optBoolean("isSystemMessage", false);
|
||||
|
||||
String messageType = item.optString("messageType", "text");
|
||||
String mediaUrl = item.optString("mediaUrl", "");
|
||||
int duration = item.optInt("duration", 0);
|
||||
|
||||
Integer burnSeconds = null;
|
||||
if (item.has("burnSeconds") && !item.isNull("burnSeconds")) {
|
||||
int v = item.optInt("burnSeconds", 0);
|
||||
if (v > 0) burnSeconds = v;
|
||||
}
|
||||
|
||||
Long viewedAt = null;
|
||||
if (item.has("viewedAt") && !item.isNull("viewedAt")) {
|
||||
long v = item.optLong("viewedAt", 0L);
|
||||
if (v > 0L) viewedAt = v;
|
||||
}
|
||||
|
||||
Long burnAt = null;
|
||||
if (item.has("burnAt") && !item.isNull("burnAt")) {
|
||||
long v = item.optLong("burnAt", 0L);
|
||||
if (v > 0L) burnAt = v;
|
||||
}
|
||||
|
||||
Boolean burned = null;
|
||||
if (item.has("burned") && !item.isNull("burned")) {
|
||||
burned = item.optBoolean("burned", false);
|
||||
}
|
||||
|
||||
// 判断是否是自己发送的消息
|
||||
// 每次都重新从 AuthStore 获取最新的 userId,确保登录后能正确获取
|
||||
String myUserId = AuthStore.getUserId(this);
|
||||
|
|
@ -591,27 +504,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus);
|
||||
chatMessage.setOutgoing(isMine); // 关键:设置消息方向,确保自己发送的消息显示在右侧
|
||||
chatMessage.setAvatarUrl(avatarUrl);
|
||||
|
||||
if ("image".equalsIgnoreCase(messageType)) {
|
||||
chatMessage.setMessageType(ChatMessage.MessageType.IMAGE);
|
||||
} else if ("voice".equalsIgnoreCase(messageType)) {
|
||||
chatMessage.setMessageType(ChatMessage.MessageType.VOICE);
|
||||
if (duration > 0) {
|
||||
chatMessage.setVoiceDuration(duration);
|
||||
}
|
||||
} else if ("burn_image".equalsIgnoreCase(messageType)) {
|
||||
chatMessage.setMessageType(ChatMessage.MessageType.BURN_IMAGE);
|
||||
chatMessage.setBurnSeconds(burnSeconds);
|
||||
chatMessage.setViewedAt(viewedAt);
|
||||
chatMessage.setBurnAt(burnAt);
|
||||
chatMessage.setBurned(burned);
|
||||
} else {
|
||||
chatMessage.setMessageType(ChatMessage.MessageType.TEXT);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mediaUrl)) {
|
||||
chatMessage.setMediaUrl(mediaUrl);
|
||||
}
|
||||
return chatMessage;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析消息失败", e);
|
||||
|
|
@ -823,13 +715,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
|
||||
binding.burnImageButton.setOnClickListener(new DebounceClickListener(300) {
|
||||
@Override
|
||||
public void onDebouncedClick(View v) {
|
||||
sendBurnImage();
|
||||
}
|
||||
});
|
||||
|
||||
binding.messageInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
sendMessage();
|
||||
|
|
@ -845,202 +730,6 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
// 设置通话按钮点击事件
|
||||
setupCallButtons();
|
||||
}
|
||||
|
||||
private void sendBurnImage() {
|
||||
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
||||
return;
|
||||
}
|
||||
if (conversationId == null) {
|
||||
Snackbar.make(binding.getRoot(), "会话ID无效", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String[] items = new String[]{"3秒", "5秒", "10秒"};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("阅后时间")
|
||||
.setItems(items, (dialog, which) -> {
|
||||
int sec;
|
||||
if (which == 0) sec = 3;
|
||||
else if (which == 1) sec = 5;
|
||||
else sec = 10;
|
||||
pendingBurnSeconds = sec;
|
||||
Intent it = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
it.setType("image/*");
|
||||
it.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(it, "选择图片"), REQ_PICK_BURN_IMAGE);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQ_PICK_BURN_IMAGE) {
|
||||
if (resultCode != RESULT_OK || data == null) {
|
||||
pendingBurnSeconds = null;
|
||||
return;
|
||||
}
|
||||
Uri uri = data.getData();
|
||||
if (uri == null) {
|
||||
pendingBurnSeconds = null;
|
||||
Snackbar.make(binding.getRoot(), "选择图片失败", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Integer sec = pendingBurnSeconds;
|
||||
pendingBurnSeconds = null;
|
||||
if (sec == null) {
|
||||
Snackbar.make(binding.getRoot(), "阅后时间无效", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
uploadAndSendBurnImage(uri, sec);
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadAndSendBurnImage(Uri imageUri, int burnSeconds) {
|
||||
ApiService api = ApiClient.getService(this);
|
||||
if (api == null) {
|
||||
Snackbar.make(binding.getRoot(), "请先登录", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
File file = copyUriToCacheFile(imageUri);
|
||||
if (file == null || !file.exists()) {
|
||||
Snackbar.make(binding.getRoot(), "读取图片失败", Snackbar.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
RequestBody requestFile = RequestBody.create(okhttp3.MediaType.parse("image/*"), file);
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
|
||||
RequestBody model = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "works");
|
||||
RequestBody pid = RequestBody.create(okhttp3.MediaType.parse("text/plain"), "0");
|
||||
|
||||
ChatMessage localMessage = ChatMessage.createBurnImageMessage("我", imageUri.toString(), burnSeconds);
|
||||
localMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
||||
messages.add(localMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
scrollToBottom();
|
||||
|
||||
api.uploadImage(body, model, pid).enqueue(new retrofit2.Callback<ApiResponse<FileUploadResponse>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<ApiResponse<FileUploadResponse>> call, retrofit2.Response<ApiResponse<FileUploadResponse>> response) {
|
||||
if (!response.isSuccessful() || response.body() == null || !response.body().isOk()) {
|
||||
String msg = response.body() != null ? response.body().getMessage() : "上传失败";
|
||||
runOnUiThread(() -> {
|
||||
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
|
||||
messages.remove(localMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
});
|
||||
return;
|
||||
}
|
||||
FileUploadResponse data = response.body().getData();
|
||||
String url = data != null ? data.getUrl() : null;
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
runOnUiThread(() -> {
|
||||
Snackbar.make(binding.getRoot(), "上传失败", Snackbar.LENGTH_SHORT).show();
|
||||
messages.remove(localMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendBurnImageToServer(url, burnSeconds, localMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<ApiResponse<FileUploadResponse>> call, Throwable t) {
|
||||
runOnUiThread(() -> {
|
||||
Snackbar.make(binding.getRoot(), "上传失败", Snackbar.LENGTH_SHORT).show();
|
||||
messages.remove(localMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendBurnImageToServer(String mediaUrl, int burnSeconds, ChatMessage localMessage) {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) return;
|
||||
|
||||
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages";
|
||||
Log.d(TAG, "发送消息: " + url);
|
||||
|
||||
try {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("message", "");
|
||||
body.put("content", "");
|
||||
body.put("messageType", "burn_image");
|
||||
body.put("mediaUrl", mediaUrl);
|
||||
body.put("burnSeconds", burnSeconds);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authori-zation", token)
|
||||
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
httpClient.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e(TAG, "发送消息失败", e);
|
||||
runOnUiThread(() -> {
|
||||
Snackbar.make(binding.getRoot(), "发送失败", Snackbar.LENGTH_SHORT).show();
|
||||
messages.remove(localMessage);
|
||||
adapter.submitList(new ArrayList<>(messages));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
Log.d(TAG, "发送消息响应: " + responseBody);
|
||||
runOnUiThread(() -> handleSendResponse(responseBody, localMessage));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "构建请求失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File copyUriToCacheFile(Uri uri) {
|
||||
try {
|
||||
String name = queryDisplayName(uri);
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
name = "burn_" + System.currentTimeMillis();
|
||||
}
|
||||
File out = new File(getCacheDir(), name);
|
||||
InputStream is = getContentResolver().openInputStream(uri);
|
||||
if (is == null) return null;
|
||||
FileOutputStream fos = new FileOutputStream(out);
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
fos.write(buf, 0, len);
|
||||
}
|
||||
fos.flush();
|
||||
fos.close();
|
||||
is.close();
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String queryDisplayName(Uri uri) {
|
||||
android.database.Cursor cursor = null;
|
||||
try {
|
||||
cursor = getContentResolver().query(uri, null, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (idx >= 0) {
|
||||
return cursor.getString(idx);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通话按钮
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
switch (type) {
|
||||
case IMAGE:
|
||||
return isOutgoing ? TYPE_IMAGE_OUTGOING : TYPE_IMAGE_INCOMING;
|
||||
case BURN_IMAGE:
|
||||
return isOutgoing ? TYPE_IMAGE_OUTGOING : TYPE_IMAGE_INCOMING;
|
||||
case VOICE:
|
||||
return isOutgoing ? TYPE_VOICE_OUTGOING : TYPE_VOICE_INCOMING;
|
||||
case TEXT:
|
||||
|
|
@ -564,11 +562,6 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
// 2. 如果为空但 message.getLocalMediaPath() 不为空,加载本地文件
|
||||
// 3. 都为空则显示占位图
|
||||
|
||||
if (message.getMessageType() != null && message.getMessageType() == ChatMessage.MessageType.BURN_IMAGE) {
|
||||
messageImageView.setImageResource(R.drawable.ic_visibility_24);
|
||||
return;
|
||||
}
|
||||
|
||||
String imageUrl = message.getMediaUrl();
|
||||
String localPath = message.getLocalMediaPath();
|
||||
|
||||
|
|
@ -714,11 +707,6 @@ public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, Recycl
|
|||
|
||||
private void loadImage(ChatMessage message) {
|
||||
if (messageImageView == null) return;
|
||||
|
||||
if (message.getMessageType() != null && message.getMessageType() == ChatMessage.MessageType.BURN_IMAGE) {
|
||||
messageImageView.setImageResource(R.drawable.ic_visibility_24);
|
||||
return;
|
||||
}
|
||||
|
||||
String imageUrl = message.getMediaUrl();
|
||||
String localPath = message.getLocalMediaPath();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,28 +3,16 @@ package com.example.livestreaming;
|
|||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityMyFriendsBinding;
|
||||
import com.example.livestreaming.location.TianDiTuLocationService;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.CommunityResponse;
|
||||
import com.example.livestreaming.net.UserEditRequest;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
|
|
@ -53,15 +41,9 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
private final List<FriendItem> allFriends = new ArrayList<>();
|
||||
private final List<FriendRequestItem> allRequests = new ArrayList<>();
|
||||
private final List<FriendItem> allBlocked = new ArrayList<>();
|
||||
private int currentTab = 0; // 0: 好友列表, 1: 好友请求, 2: 附近, 3: 黑名单
|
||||
private int currentTab = 0; // 0: 好友列表, 1: 好友请求, 2: 黑名单
|
||||
private OkHttpClient httpClient;
|
||||
|
||||
// 附近Tab相关
|
||||
private NearbyUsersAdapter nearbyUsersAdapter;
|
||||
private final List<NearbyUser> nearbyUsers = new ArrayList<>();
|
||||
private TianDiTuLocationService locationService;
|
||||
private ActivityResultLauncher<String[]> requestLocationPermissionLauncher;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
|
@ -69,31 +51,12 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
setContentView(binding.getRoot());
|
||||
|
||||
httpClient = new OkHttpClient();
|
||||
locationService = new TianDiTuLocationService(this);
|
||||
setupLaunchers();
|
||||
|
||||
setupViews();
|
||||
setupTabs();
|
||||
loadFriendList();
|
||||
}
|
||||
|
||||
private void setupLaunchers() {
|
||||
requestLocationPermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||
result -> {
|
||||
Boolean fine = result.get(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
Boolean coarse = result.get(Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
boolean granted = (fine != null && fine) || (coarse != null && coarse);
|
||||
if (granted) {
|
||||
updateMyLocationAndReloadNearby();
|
||||
} else {
|
||||
Toast.makeText(this, "需要位置权限才能查看附近的人", Toast.LENGTH_SHORT).show();
|
||||
updateNearbyPermissionUi();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void setupViews() {
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
|
|
@ -156,7 +119,6 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
private void setupTabs() {
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("好友列表"));
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("好友请求"));
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("附近"));
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("黑名单"));
|
||||
|
||||
binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
|
|
@ -177,274 +139,23 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
binding.friendsRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.requestsRecyclerView.setVisibility(View.GONE);
|
||||
binding.blockedRecyclerView.setVisibility(View.GONE);
|
||||
binding.nearbyTabContainer.getRoot().setVisibility(View.GONE);
|
||||
binding.searchContainer.setVisibility(View.VISIBLE);
|
||||
loadFriendList();
|
||||
} else if (tabIndex == 1) {
|
||||
binding.friendsRecyclerView.setVisibility(View.GONE);
|
||||
binding.requestsRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.blockedRecyclerView.setVisibility(View.GONE);
|
||||
binding.nearbyTabContainer.getRoot().setVisibility(View.GONE);
|
||||
binding.searchContainer.setVisibility(View.GONE);
|
||||
loadFriendRequests();
|
||||
} else if (tabIndex == 2) {
|
||||
binding.friendsRecyclerView.setVisibility(View.GONE);
|
||||
binding.requestsRecyclerView.setVisibility(View.GONE);
|
||||
binding.blockedRecyclerView.setVisibility(View.GONE);
|
||||
binding.nearbyTabContainer.getRoot().setVisibility(View.VISIBLE);
|
||||
binding.searchContainer.setVisibility(View.GONE);
|
||||
showNearbyTab();
|
||||
} else {
|
||||
binding.friendsRecyclerView.setVisibility(View.GONE);
|
||||
binding.requestsRecyclerView.setVisibility(View.GONE);
|
||||
binding.blockedRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.nearbyTabContainer.getRoot().setVisibility(View.GONE);
|
||||
binding.searchContainer.setVisibility(View.GONE);
|
||||
loadBlockedList();
|
||||
}
|
||||
}
|
||||
|
||||
private void showNearbyTab() {
|
||||
ensureNearbyAdapter();
|
||||
bindNearbyViews();
|
||||
updateNearbyPermissionUi();
|
||||
loadNearbyUsersSimple(false);
|
||||
}
|
||||
|
||||
private void ensureNearbyAdapter() {
|
||||
if (nearbyUsersAdapter != null) return;
|
||||
nearbyUsersAdapter = new NearbyUsersAdapter(user -> {
|
||||
if (user == null) return;
|
||||
UserProfileReadOnlyActivity.start(MyFriendsActivity.this,
|
||||
user.getId(),
|
||||
user.getName(),
|
||||
user.getDistanceText(),
|
||||
"",
|
||||
"");
|
||||
});
|
||||
nearbyUsersAdapter.setOnAddFriendClickListener(user -> {
|
||||
if (user == null) return;
|
||||
sendFriendRequestToNearbyUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
private void sendFriendRequestToNearbyUser(NearbyUser user) {
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
AuthHelper.requireLogin(this, "添加好友需要登录");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int userId = Integer.parseInt(user.getId());
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("targetUserId", userId);
|
||||
body.put("message", "我想加你为好友");
|
||||
|
||||
ApiClient.getService(getApplicationContext()).sendFriendRequest(body)
|
||||
.enqueue(new retrofit2.Callback<ApiResponse<Boolean>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<ApiResponse<Boolean>> call, retrofit2.Response<ApiResponse<Boolean>> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||||
Toast.makeText(MyFriendsActivity.this, "好友请求已发送", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
String msg = response.body() != null && response.body().getMessage() != null
|
||||
? response.body().getMessage() : "发送失败";
|
||||
Toast.makeText(MyFriendsActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<ApiResponse<Boolean>> call, Throwable t) {
|
||||
Toast.makeText(MyFriendsActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "用户ID格式错误", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindNearbyViews() {
|
||||
View root = binding.nearbyTabContainer.getRoot();
|
||||
RecyclerView list = root.findViewById(R.id.nearbyContentList);
|
||||
SwipeRefreshLayout swipe = root.findViewById(R.id.nearbySwipeRefresh);
|
||||
View btnRefresh = root.findViewById(R.id.btnOpenLocal);
|
||||
View btnEnableLocation = root.findViewById(R.id.btnEnableLocation);
|
||||
|
||||
if (list != null && list.getAdapter() == null) {
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(nearbyUsersAdapter);
|
||||
}
|
||||
if (swipe != null) {
|
||||
swipe.setOnRefreshListener(() -> loadNearbyUsersSimple(true));
|
||||
}
|
||||
if (btnRefresh != null) {
|
||||
btnRefresh.setOnClickListener(v -> loadNearbyUsersSimple(false));
|
||||
}
|
||||
if (btnEnableLocation != null) {
|
||||
btnEnableLocation.setOnClickListener(v -> requestLocationPermission());
|
||||
}
|
||||
}
|
||||
|
||||
private void requestLocationPermission() {
|
||||
requestLocationPermissionLauncher.launch(new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasLocationPermission() {
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void updateNearbyPermissionUi() {
|
||||
View root = binding.nearbyTabContainer.getRoot();
|
||||
View empty = root.findViewById(R.id.emptyNearbyContainer);
|
||||
View loading = root.findViewById(R.id.nearbyLoadingContainer);
|
||||
RecyclerView list = root.findViewById(R.id.nearbyContentList);
|
||||
View btnEnableLocation = root.findViewById(R.id.btnEnableLocation);
|
||||
|
||||
if (loading != null) loading.setVisibility(View.GONE);
|
||||
|
||||
boolean hasPermission = hasLocationPermission();
|
||||
if (btnEnableLocation != null) {
|
||||
btnEnableLocation.setVisibility(hasPermission ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!hasPermission) {
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
if (empty != null) empty.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopNearbySwipeRefresh() {
|
||||
SwipeRefreshLayout swipe = binding.nearbyTabContainer.getRoot().findViewById(R.id.nearbySwipeRefresh);
|
||||
if (swipe != null && swipe.isRefreshing()) {
|
||||
swipe.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMyLocationAndReloadNearby() {
|
||||
locationService.startLocation(new TianDiTuLocationService.OnLocationResultListener() {
|
||||
@Override
|
||||
public void onSuccess(String province, String city, String address, double latitude, double longitude) {
|
||||
String addres;
|
||||
if (province != null && !province.isEmpty() && city != null && !city.isEmpty()) {
|
||||
addres = province + "-" + city;
|
||||
} else if (address != null && !address.isEmpty()) {
|
||||
addres = address;
|
||||
} else {
|
||||
addres = null;
|
||||
}
|
||||
|
||||
UserEditRequest req = new UserEditRequest();
|
||||
req.setAddres(addres);
|
||||
req.setLatitude(latitude);
|
||||
req.setLongitude(longitude);
|
||||
|
||||
ApiClient.getService(getApplicationContext()).updateUserInfo(req)
|
||||
.enqueue(new retrofit2.Callback<ApiResponse<Object>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<ApiResponse<Object>> call, retrofit2.Response<ApiResponse<Object>> response) {
|
||||
loadNearbyUsersSimple(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<ApiResponse<Object>> call, Throwable t) {
|
||||
loadNearbyUsersSimple(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
runOnUiThread(() -> {
|
||||
Toast.makeText(MyFriendsActivity.this, error != null ? error : "定位失败", Toast.LENGTH_SHORT).show();
|
||||
loadNearbyUsersSimple(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadNearbyUsersSimple(boolean fromSwipeRefresh) {
|
||||
View root = binding.nearbyTabContainer.getRoot();
|
||||
View empty = root.findViewById(R.id.emptyNearbyContainer);
|
||||
View loading = root.findViewById(R.id.nearbyLoadingContainer);
|
||||
RecyclerView list = root.findViewById(R.id.nearbyContentList);
|
||||
|
||||
if (!hasLocationPermission()) {
|
||||
updateNearbyPermissionUi();
|
||||
stopNearbySwipeRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
stopNearbySwipeRefresh();
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
if (empty != null) empty.setVisibility(View.VISIBLE);
|
||||
Toast.makeText(this, "登录后查看附近的人", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fromSwipeRefresh) {
|
||||
if (loading != null) loading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (empty != null) empty.setVisibility(View.GONE);
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
|
||||
ApiClient.getService(getApplicationContext()).getNearbyUsersSimple(100)
|
||||
.enqueue(new retrofit2.Callback<ApiResponse<CommunityResponse.NearbyUserSimpleList>>() {
|
||||
@Override
|
||||
public void onResponse(retrofit2.Call<ApiResponse<CommunityResponse.NearbyUserSimpleList>> call,
|
||||
retrofit2.Response<ApiResponse<CommunityResponse.NearbyUserSimpleList>> response) {
|
||||
runOnUiThread(() -> {
|
||||
stopNearbySwipeRefresh();
|
||||
if (loading != null) loading.setVisibility(View.GONE);
|
||||
|
||||
if (!response.isSuccessful() || response.body() == null || !response.body().isOk()) {
|
||||
if (empty != null) empty.setVisibility(View.VISIBLE);
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
CommunityResponse.NearbyUserSimpleList data = response.body().getData();
|
||||
List<CommunityResponse.NearbyUserSimple> users = data != null ? data.getUsers() : null;
|
||||
nearbyUsers.clear();
|
||||
|
||||
if (users != null) {
|
||||
for (CommunityResponse.NearbyUserSimple u : users) {
|
||||
String address = u.address != null ? u.address : "未设置位置";
|
||||
String distanceText = u.distanceText != null ? u.distanceText : "";
|
||||
String locationInfo = !distanceText.isEmpty() ? (address + " · " + distanceText) : address;
|
||||
nearbyUsers.add(new NearbyUser(String.valueOf(u.id), u.nickname, locationInfo, false));
|
||||
}
|
||||
}
|
||||
|
||||
if (!nearbyUsers.isEmpty()) {
|
||||
if (nearbyUsersAdapter != null) {
|
||||
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
|
||||
}
|
||||
if (list != null) list.setVisibility(View.VISIBLE);
|
||||
if (empty != null) empty.setVisibility(View.GONE);
|
||||
} else {
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
if (empty != null) empty.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(retrofit2.Call<ApiResponse<CommunityResponse.NearbyUserSimpleList>> call, Throwable t) {
|
||||
runOnUiThread(() -> {
|
||||
stopNearbySwipeRefresh();
|
||||
if (loading != null) loading.setVisibility(View.GONE);
|
||||
if (empty != null) empty.setVisibility(View.VISIBLE);
|
||||
if (list != null) list.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadFriendList() {
|
||||
String token = AuthStore.getToken(this);
|
||||
if (token == null) {
|
||||
|
|
|
|||
|
|
@ -630,9 +630,7 @@ public class PublishWorkActivity extends AppCompatActivity {
|
|||
binding.emptyMediaContainer.setVisibility(View.VISIBLE);
|
||||
binding.mediaRecyclerView.setVisibility(View.GONE);
|
||||
binding.videoPreviewContainer.setVisibility(View.GONE);
|
||||
// 允许只选封面发布:无媒体时也展示封面选择区域
|
||||
binding.coverCard.setVisibility(View.VISIBLE);
|
||||
updateCoverPreview();
|
||||
binding.coverCard.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -743,7 +741,14 @@ public class PublishWorkActivity extends AppCompatActivity {
|
|||
android.util.Log.d("PublishWork", "封面URI: " + (selectedCoverUri != null ? selectedCoverUri.toString() : "null"));
|
||||
android.util.Log.d("PublishWork", "封面是否本地文件: " + (selectedCoverUri != null ? isLocalUri(selectedCoverUri) : "N/A"));
|
||||
|
||||
// 验证标题长度(标题允许为空)
|
||||
// 验证标题
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
Toast.makeText(this, "请输入作品标题", Toast.LENGTH_SHORT).show();
|
||||
binding.titleEditText.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证标题长度
|
||||
if (title.length() > 20) {
|
||||
Toast.makeText(this, "标题最多20字", Toast.LENGTH_SHORT).show();
|
||||
binding.titleEditText.requestFocus();
|
||||
|
|
@ -757,9 +762,9 @@ public class PublishWorkActivity extends AppCompatActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
// 验证媒体/封面:允许只选封面发布
|
||||
if (selectedMediaUris.isEmpty() && selectedCoverUri == null) {
|
||||
Toast.makeText(this, "请选择封面", Toast.LENGTH_SHORT).show();
|
||||
// 验证媒体
|
||||
if (selectedMediaUris.isEmpty()) {
|
||||
Toast.makeText(this, "请选择图片或视频", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -775,11 +780,7 @@ public class PublishWorkActivity extends AppCompatActivity {
|
|||
progressDialog.show();
|
||||
|
||||
// 开始上传流程
|
||||
if (selectedMediaUris.isEmpty() && selectedCoverUri != null) {
|
||||
// 只选封面发布:按图片作品发布,图片列表允许为空
|
||||
currentWorkType = WorkItem.WorkType.IMAGE;
|
||||
handleImageWorkUpload(title, description, progressDialog);
|
||||
} else if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) {
|
||||
if (currentWorkType == WorkItem.WorkType.VIDEO && selectedVideoUri != null) {
|
||||
// 视频作品处理
|
||||
handleVideoWorkUpload(title, description, progressDialog);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -165,16 +165,6 @@ public class WorksAdapter extends RecyclerView.Adapter<WorksAdapter.WorksViewHol
|
|||
typeIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 显示热门标识
|
||||
ImageView hotBadge = itemView.findViewById(R.id.hotBadge);
|
||||
if (hotBadge != null) {
|
||||
if (works.isHot()) {
|
||||
hotBadge.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
hotBadge.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置点击事件
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
|
|
|
|||
|
|
@ -170,9 +170,6 @@ public interface ApiService {
|
|||
@DELETE("api/front/conversations/messages/{id}")
|
||||
Call<ApiResponse<Boolean>> deleteMessage(@Path("id") long id);
|
||||
|
||||
@POST("api/front/conversations/messages/{id}/burn/view")
|
||||
Call<ApiResponse<BurnViewResponse>> viewBurnImage(@Path("id") long id);
|
||||
|
||||
// ==================== 好友管理 ====================
|
||||
|
||||
@GET("api/front/friends")
|
||||
|
|
@ -380,14 +377,6 @@ public interface ApiService {
|
|||
@POST("api/front/works/update")
|
||||
Call<ApiResponse<Boolean>> updateWork(@Body WorksRequest body);
|
||||
|
||||
/**
|
||||
* 获取附近作品列表(按距离从近到远排序)
|
||||
*/
|
||||
@GET("api/front/works/nearby")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getNearbyWorks(
|
||||
@Query("page") int page,
|
||||
@Query("limit") int limit);
|
||||
|
||||
@GET("api/front/works/detail/{id}")
|
||||
Call<ApiResponse<WorksResponse>> getWorkDetail(@Path("id") long id);
|
||||
|
||||
|
|
@ -436,14 +425,6 @@ public interface ApiService {
|
|||
@POST("api/front/works/comment/delete/{commentId}")
|
||||
Call<ApiResponse<Boolean>> deleteWorkComment(@Path("commentId") long commentId);
|
||||
|
||||
/**
|
||||
* 获取热门作品列表
|
||||
*/
|
||||
@GET("api/front/works/hot")
|
||||
Call<ApiResponse<PageResponse<WorksResponse>>> getHotWorks(
|
||||
@Query("page") int page,
|
||||
@Query("pageSize") int pageSize);
|
||||
|
||||
@POST("api/front/works/comment/like/{commentId}")
|
||||
Call<ApiResponse<Boolean>> likeWorkComment(@Path("commentId") long commentId);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class BurnViewResponse {
|
||||
|
||||
@SerializedName("messageId")
|
||||
private Long messageId;
|
||||
|
||||
@SerializedName("mediaUrl")
|
||||
private String mediaUrl;
|
||||
|
||||
@SerializedName("burnSeconds")
|
||||
private Integer burnSeconds;
|
||||
|
||||
@SerializedName("viewedAt")
|
||||
private Long viewedAt;
|
||||
|
||||
@SerializedName("burnAt")
|
||||
private Long burnAt;
|
||||
|
||||
@SerializedName("burned")
|
||||
private Boolean burned;
|
||||
|
||||
public Long getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public String getMediaUrl() {
|
||||
return mediaUrl;
|
||||
}
|
||||
|
||||
public Integer getBurnSeconds() {
|
||||
return burnSeconds;
|
||||
}
|
||||
|
||||
public Long getViewedAt() {
|
||||
return viewedAt;
|
||||
}
|
||||
|
||||
public Long getBurnAt() {
|
||||
return burnAt;
|
||||
}
|
||||
|
||||
public Boolean getBurned() {
|
||||
return burned;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,8 +27,6 @@ public class WorksResponse {
|
|||
private Boolean isLiked; // 当前用户是否已点赞
|
||||
private Boolean isCollected; // 当前用户是否已收藏
|
||||
private Boolean isOwner; // 是否是当前用户的作品
|
||||
private Integer isHot; // 是否热门:1-是 0-否
|
||||
private String hotTime; // 设置热门的时间
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
|
|
@ -202,25 +200,4 @@ public class WorksResponse {
|
|||
public void setIsOwner(Boolean isOwner) {
|
||||
this.isOwner = isOwner;
|
||||
}
|
||||
|
||||
public Integer getIsHot() {
|
||||
return isHot != null ? isHot : 0;
|
||||
}
|
||||
|
||||
public void setIsHot(Integer isHot) {
|
||||
this.isHot = isHot;
|
||||
}
|
||||
|
||||
// 便利方法:判断是否热门
|
||||
public boolean isHot() {
|
||||
return isHot != null && isHot == 1;
|
||||
}
|
||||
|
||||
public String getHotTime() {
|
||||
return hotTime;
|
||||
}
|
||||
|
||||
public void setHotTime(String hotTime) {
|
||||
this.hotTime = hotTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.5,0.67s0.74,2.65 0.74,4.8c0,2.06 -1.35,3.73 -3.41,3.73 -2.07,0 -3.63,-1.67 -3.63,-3.73l0.03,-0.36C5.21,7.51 4,10.62 4,14c0,4.42 3.58,8 8,8s8,-3.58 8,-8C20,8.61 17.41,3.8 13.5,0.67zM11.71,19c-1.78,0 -3.22,-1.4 -3.22,-3.14 0,-1.62 1.05,-2.76 2.81,-3.12 1.77,-0.36 3.6,-1.21 4.62,-2.58 0.39,1.29 0.59,2.65 0.59,4.04 0,2.65 -2.15,4.8 -4.8,4.8z"/>
|
||||
</vector>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000000">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="image"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/countdownText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="16dp"
|
||||
android:background="#66000000"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tipText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -123,15 +123,6 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/burnImageButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="阅后即焚图片"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_lock_12" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/messageInput"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/topTabs">
|
||||
|
||||
<!-- 分类标签 - 显示6个标签 -->
|
||||
<!-- 分类标签 - 显示5个标签 -->
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/categoryTabs"
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -174,11 +174,6 @@
|
|||
app:tabSelectedTextColor="#333333"
|
||||
app:tabTextColor="#999999">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="热门" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -146,14 +146,6 @@
|
|||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<include
|
||||
android:id="@+id/nearbyTabContainer"
|
||||
layout="@layout/layout_nearby_tab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="visible"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="#FFFFFF">
|
||||
|
|
|
|||
|
|
@ -35,18 +35,6 @@
|
|||
android:visibility="gone"
|
||||
android:contentDescription="视频" />
|
||||
|
||||
<!-- 热门标识 -->
|
||||
<ImageView
|
||||
android:id="@+id/hotBadge"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_fire_24"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="热门"
|
||||
app:tint="#FF4500" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- 作品信息 -->
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@
|
|||
android:id="@+id/nearbyCountText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="正在搜索附近的人..."
|
||||
android:textSize="13sp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text=""
|
||||
android:textSize="12sp"
|
||||
android:textColor="#999999" />
|
||||
</LinearLayout>
|
||||
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="正在加载附近的人..."
|
||||
android:text="正在搜索附近的人..."
|
||||
android:textSize="13sp"
|
||||
android:textColor="#999999" />
|
||||
</LinearLayout>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user