修复了一些bug和添加了TODO
This commit is contained in:
parent
484c17a4d3
commit
171054efbb
|
|
@ -138,6 +138,16 @@
|
|||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.LoginActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.RegisterActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.MainActivity"
|
||||
android:exported="true"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
|
||||
/**
|
||||
* 登录检查工具类
|
||||
* 用于在需要登录的功能处检查登录状态,如果未登录则提示用户并跳转到登录页面
|
||||
*/
|
||||
public class AuthHelper {
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
* @param context 上下文
|
||||
* @return true表示已登录,false表示未登录
|
||||
*/
|
||||
public static boolean isLoggedIn(Context context) {
|
||||
if (context == null) return false;
|
||||
String token = AuthStore.getToken(context);
|
||||
return !TextUtils.isEmpty(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,如果未登录则提示用户并跳转到登录页面
|
||||
* @param context 上下文(必须是Activity)
|
||||
* @param message 提示消息(可选,如果为null则使用默认消息)
|
||||
* @return true表示已登录,false表示未登录(已跳转到登录页面)
|
||||
*/
|
||||
public static boolean requireLogin(Context context, String message) {
|
||||
if (isLoggedIn(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未登录,显示提示对话框
|
||||
String tipMessage = message != null ? message : "此功能需要登录后使用,请先登录";
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("需要登录")
|
||||
.setMessage(tipMessage)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("去登录", (dialog, which) -> {
|
||||
// 跳转到登录页面
|
||||
LoginActivity.start(context);
|
||||
})
|
||||
.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,如果未登录则提示用户并跳转到登录页面(使用默认提示消息)
|
||||
* @param context 上下文(必须是Activity)
|
||||
* @return true表示已登录,false表示未登录(已跳转到登录页面)
|
||||
*/
|
||||
public static boolean requireLogin(Context context) {
|
||||
return requireLogin(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,如果未登录则显示Toast提示并跳转到登录页面
|
||||
* @param context 上下文(必须是Activity)
|
||||
* @param toastMessage Toast提示消息(可选,如果为null则使用默认消息)
|
||||
* @return true表示已登录,false表示未登录(已跳转到登录页面)
|
||||
*/
|
||||
public static boolean requireLoginWithToast(Context context, String toastMessage) {
|
||||
if (isLoggedIn(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未登录,显示Toast提示并跳转到登录页面
|
||||
String message = toastMessage != null ? toastMessage : "此功能需要登录后使用";
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
LoginActivity.start(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录状态,如果未登录则显示Toast提示并跳转到登录页面(使用默认提示消息)
|
||||
* @param context 上下文(必须是Activity)
|
||||
* @return true表示已登录,false表示未登录(已跳转到登录页面)
|
||||
*/
|
||||
public static boolean requireLoginWithToast(Context context) {
|
||||
return requireLoginWithToast(context, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +27,20 @@ public class CategoryFilterManager {
|
|||
|
||||
/**
|
||||
* 异步筛选房间列表
|
||||
* TODO: 接入后端接口 - 获取房间分类列表
|
||||
* 接口路径: GET /api/rooms/categories
|
||||
* 请求参数: 无
|
||||
* 返回数据格式: ApiResponse<List<Category>>
|
||||
* Category对象应包含: id, name, iconUrl, roomCount等字段
|
||||
* 用于显示分类标签页,分类数据应从后端获取,而不是硬编码
|
||||
* TODO: 接入后端接口 - 按分类获取房间列表
|
||||
* 接口路径: GET /api/rooms?category={categoryId}
|
||||
* 请求参数:
|
||||
* - categoryId: 分类ID(路径参数或查询参数)
|
||||
* - page (可选): 页码
|
||||
* - pageSize (可选): 每页数量
|
||||
* 返回数据格式: ApiResponse<List<Room>>
|
||||
* 筛选逻辑应迁移到后端,前端只负责展示结果
|
||||
*/
|
||||
public void filterRoomsAsync(List<Room> allRooms, String category, FilterCallback callback) {
|
||||
if (executorService == null || executorService.isShutdown()) {
|
||||
|
|
|
|||
|
|
@ -188,6 +188,11 @@ public class ConversationActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void sendMessage() {
|
||||
// 检查登录状态,发送私信需要登录
|
||||
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 发送私信消息
|
||||
// 接口路径: POST /api/conversations/{conversationId}/messages
|
||||
// 请求参数:
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ public class EditProfileActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 检查登录状态,编辑资料需要登录
|
||||
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
|
@ -136,6 +143,24 @@ public class EditProfileActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
binding.saveButton.setOnClickListener(v -> {
|
||||
// TODO: 接入后端接口 - 上传头像
|
||||
// 接口路径: POST /api/users/{userId}/avatar
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - avatar: 头像文件(multipart/form-data)
|
||||
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||
// 上传成功后,保存avatarUrl到本地,并更新界面显示
|
||||
// TODO: 接入后端接口 - 更新用户资料
|
||||
// 接口路径: PUT /api/users/{userId}/profile
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - name: 昵称
|
||||
// - bio: 个人签名
|
||||
// - birthday: 生日(格式:yyyy-MM-dd)
|
||||
// - gender: 性别(男/女/保密)
|
||||
// - location: 所在地(格式:省份-城市)
|
||||
// 返回数据格式: ApiResponse<UserProfile>
|
||||
// 更新成功后,同步更新本地缓存和界面显示
|
||||
String name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
|
||||
String bio = binding.inputBio.getText() != null ? binding.inputBio.getText().toString().trim() : "";
|
||||
String birthday = binding.inputBirthday.getText() != null ? binding.inputBirthday.getText().toString().trim() : "";
|
||||
|
|
@ -197,6 +222,15 @@ public class EditProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private Uri persistAvatarToInternalStorage(Uri sourceUri) {
|
||||
// TODO: 接入后端接口 - 上传头像到服务器
|
||||
// 接口路径: POST /api/upload/image
|
||||
// 请求参数:
|
||||
// - file: 图片文件(multipart/form-data)
|
||||
// - model: 模块类型(如"user")
|
||||
// - pid: 分类ID(如7表示前台用户)
|
||||
// 返回数据格式: ApiResponse<{url: string}>
|
||||
// 上传成功后,保存返回的URL到本地,并更新界面显示
|
||||
// 注意:这里目前只是保存到本地,实际应该先上传到服务器,然后保存服务器返回的URL
|
||||
if (sourceUri == null) return null;
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ public class FriendsAdapter extends ListAdapter<FriendItem, FriendsAdapter.VH> {
|
|||
}
|
||||
|
||||
void bind(FriendItem item) {
|
||||
// TODO: 接入后端接口 - 从后端获取好友头像URL
|
||||
// 接口路径: GET /api/user/profile/{friendId}
|
||||
// 请求参数: friendId (路径参数,从FriendItem对象中获取)
|
||||
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||
// 使用Glide加载头像,如果没有则使用默认占位图
|
||||
// TODO: 接入后端接口 - 获取好友在线状态
|
||||
// 接口路径: GET /api/user/status/{friendId}
|
||||
// 请求参数: friendId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{isOnline: boolean, lastActiveTime: timestamp}>
|
||||
// 或者FriendItem对象应包含isOnline字段,直接从item.isOnline()获取
|
||||
binding.name.setText(item != null && item.getName() != null ? item.getName() : "");
|
||||
binding.subtitle.setText(item != null && item.getSubtitle() != null ? item.getSubtitle() : "");
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ public class LocalNotificationManager {
|
|||
|
||||
/**
|
||||
* 发送通知
|
||||
* TODO: 接入后端接口 - 同步通知到后端
|
||||
* 接口路径: POST /api/notifications/sync
|
||||
* 请求参数:
|
||||
* - userId: 用户ID(从token中获取)
|
||||
* - notificationId: 通知ID(本地生成的唯一ID)
|
||||
* - type: 通知类型
|
||||
* - title: 通知标题
|
||||
* - content: 通知内容
|
||||
* - timestamp: 通知时间戳
|
||||
* 返回数据格式: ApiResponse<{success: boolean, serverNotificationId: string}>
|
||||
* 用于将本地通知同步到后端,确保多设备间通知状态一致
|
||||
* 注意:本地通知仍应正常发送,同步失败不应影响本地通知显示
|
||||
*/
|
||||
public static void sendNotification(Context context, String title, String content, NotificationItem.Type type) {
|
||||
if (context == null || title == null || content == null) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityLoginBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.example.livestreaming.net.LoginRequest;
|
||||
import com.example.livestreaming.net.LoginResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class LoginActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityLoginBinding binding;
|
||||
private boolean isLoggingIn = false;
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, LoginActivity.class);
|
||||
// 不使用CLEAR_TOP和NEW_TASK,保留Activity栈,允许用户返回上一页
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityLoginBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setupUI();
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
// 返回按钮点击事件
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
// 登录按钮点击事件
|
||||
binding.loginButton.setOnClickListener(v -> performLogin());
|
||||
|
||||
// 注册链接点击事件
|
||||
binding.registerLinkText.setOnClickListener(v -> {
|
||||
RegisterActivity.start(LoginActivity.this);
|
||||
});
|
||||
|
||||
// 忘记密码点击事件
|
||||
binding.forgotPasswordText.setOnClickListener(v -> {
|
||||
Toast.makeText(this, "忘记密码功能待开发", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// 回车键登录
|
||||
binding.passwordInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_DONE) {
|
||||
performLogin();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void performLogin() {
|
||||
// 防止重复提交
|
||||
if (isLoggingIn) return;
|
||||
|
||||
String account = binding.accountInput.getText() != null ?
|
||||
binding.accountInput.getText().toString().trim() : "";
|
||||
String password = binding.passwordInput.getText() != null ?
|
||||
binding.passwordInput.getText().toString().trim() : "";
|
||||
|
||||
// 验证输入
|
||||
if (TextUtils.isEmpty(account)) {
|
||||
binding.accountLayout.setError("请输入账号");
|
||||
binding.accountInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.accountLayout.setError(null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.passwordLayout.setError("请输入密码");
|
||||
binding.passwordInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.passwordLayout.setError(null);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
isLoggingIn = true;
|
||||
binding.loginButton.setEnabled(false);
|
||||
binding.loadingProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
// TODO: 接入后端接口 - 用户登录
|
||||
// 接口路径: POST /api/front/login(ApiService中已定义)
|
||||
// 请求参数: LoginRequest {account: string, password: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
|
||||
// 登录成功后,保存token到AuthStore,并更新用户信息到本地SharedPreferences
|
||||
ApiClient.getService(getApplicationContext()).login(new LoginRequest(account, password))
|
||||
.enqueue(new Callback<ApiResponse<LoginResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<LoginResponse>> call, Response<ApiResponse<LoginResponse>> response) {
|
||||
isLoggingIn = false;
|
||||
binding.loginButton.setEnabled(true);
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
|
||||
ApiResponse<LoginResponse> body = response.body();
|
||||
LoginResponse loginData = body != null ? body.getData() : null;
|
||||
|
||||
// 如果响应不成功或数据无效,检查是否是后端未接入的情况
|
||||
if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
|
||||
// 如果是404、500等错误,可能是后端未接入,使用演示模式
|
||||
if (!response.isSuccessful() && (response.code() == 404 || response.code() == 500 || response.code() == 502 || response.code() == 503)) {
|
||||
// 后端服务未启动或未接入,使用演示模式
|
||||
handleDemoModeLogin(account);
|
||||
return;
|
||||
}
|
||||
|
||||
String errorMsg = "登录失败";
|
||||
if (body != null && !TextUtils.isEmpty(body.getMessage())) {
|
||||
errorMsg = body.getMessage();
|
||||
} else if (!response.isSuccessful()) {
|
||||
errorMsg = "网络错误:" + response.code();
|
||||
}
|
||||
Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存token
|
||||
String token = loginData.getToken();
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
AuthStore.setToken(getApplicationContext(), token);
|
||||
}
|
||||
|
||||
// 保存用户信息到本地(如果LoginResponse包含用户信息)
|
||||
// 注意:这里假设LoginResponse可能包含userId和nickname,如果后端返回了这些字段,需要在这里保存
|
||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
||||
// 如果后端返回了nickname,可以在这里保存
|
||||
// prefs.edit().putString("profile_name", loginData.getNickname()).apply();
|
||||
|
||||
// 登录成功,返回上一页(如果是从其他页面跳转过来的)
|
||||
// 如果是直接打开登录页面,则跳转到主页面
|
||||
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 检查是否有上一个Activity
|
||||
if (isTaskRoot()) {
|
||||
// 如果没有上一个Activity,跳转到主页面
|
||||
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else {
|
||||
// 有上一个Activity,直接返回
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<LoginResponse>> call, Throwable t) {
|
||||
isLoggingIn = false;
|
||||
binding.loginButton.setEnabled(true);
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
|
||||
// 检测是否是网络连接错误(后端未启动)
|
||||
boolean isNetworkError = false;
|
||||
String errorMsg = "网络错误";
|
||||
if (t != null) {
|
||||
String msg = t.getMessage();
|
||||
if (msg != null) {
|
||||
if (msg.contains("Unable to resolve host") ||
|
||||
msg.contains("timeout") ||
|
||||
msg.contains("Connection refused") ||
|
||||
msg.contains("Failed to connect")) {
|
||||
isNetworkError = true;
|
||||
errorMsg = "无法连接到服务器,已切换到演示模式";
|
||||
} else {
|
||||
errorMsg = "网络错误:" + msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是网络连接错误(后端未接入),使用演示模式登录
|
||||
if (isNetworkError) {
|
||||
handleDemoModeLogin(account);
|
||||
} else {
|
||||
Toast.makeText(LoginActivity.this, errorMsg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理演示模式登录
|
||||
*/
|
||||
private void handleDemoModeLogin(String account) {
|
||||
// 演示模式:允许任意账号密码登录(仅用于开发测试)
|
||||
Toast.makeText(this, "演示模式:后端未接入,使用演示账号登录", Toast.LENGTH_LONG).show();
|
||||
|
||||
// 生成一个演示token(基于账号)
|
||||
String demoToken = "demo_token_" + account.hashCode();
|
||||
AuthStore.setToken(getApplicationContext(), demoToken);
|
||||
|
||||
// 保存用户信息到本地
|
||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
||||
prefs.edit()
|
||||
.putString("profile_name", account.length() > 0 ? account : "演示用户")
|
||||
.apply();
|
||||
|
||||
// 登录成功,返回上一页(如果是从其他页面跳转过来的)
|
||||
// 如果是直接打开登录页面,则跳转到主页面
|
||||
Toast.makeText(this, "演示模式登录成功", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 检查是否有上一个Activity
|
||||
if (isTaskRoot()) {
|
||||
// 如果没有上一个Activity,跳转到主页面
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else {
|
||||
// 有上一个Activity,直接返回
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +90,20 @@ public class MainActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 用户打开APP时不需要强制登录,可以直接使用APP
|
||||
// 只有在使用需要登录的功能时(如加好友、发送弹幕等),才检查登录状态
|
||||
// TODO: 接入后端接口 - 用户登录
|
||||
// 接口路径: POST /api/front/login(ApiService中已定义)
|
||||
// 请求参数: LoginRequest {account: string, password: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
|
||||
// 登录成功后,保存token到AuthStore,并更新用户信息
|
||||
// TODO: 接入后端接口 - 用户注册
|
||||
// 接口路径: POST /api/front/register(ApiService中已定义)
|
||||
// 请求参数: RegisterRequest {phone: string, password: string, verificationCode: string, nickname: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// 注册成功后,自动登录并保存token
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
|
@ -173,49 +187,81 @@ public class MainActivity extends AppCompatActivity {
|
|||
private void handleDrawerAction(DrawerCardItem item) {
|
||||
int action = item.getAction();
|
||||
if (action == DrawerCardItem.ACTION_PROFILE) {
|
||||
// 检查登录状态,个人主页需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||
return;
|
||||
}
|
||||
ProfileActivity.start(this);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_MESSAGES) {
|
||||
// MessagesActivity内部已检查登录,这里直接跳转
|
||||
MessagesActivity.start(this);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_MY_FRIENDS) {
|
||||
// 检查登录状态,我的好友需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看好友列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
startActivity(new Intent(this, MyFriendsActivity.class));
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_FISH_POND) {
|
||||
// 检查登录状态,缘池功能需要登录
|
||||
if (!AuthHelper.requireLogin(this, "缘池功能需要登录")) {
|
||||
return;
|
||||
}
|
||||
startActivity(new Intent(this, FishPondActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_FOLLOWING) {
|
||||
// 检查登录状态,我的关注需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
FollowingListActivity.start(this);
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_FANS) {
|
||||
// 检查登录状态,我的粉丝需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看粉丝列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
FansListActivity.start(this);
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_LIKES) {
|
||||
// 检查登录状态,获赞列表需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看获赞列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
LikesListActivity.start(this);
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_HISTORY) {
|
||||
// 检查登录状态,观看历史需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
|
||||
return;
|
||||
}
|
||||
WatchHistoryActivity.start(this);
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_SEARCH) {
|
||||
// 搜索功能不需要登录,游客也可以搜索
|
||||
SearchActivity.start(this);
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_SETTINGS) {
|
||||
// SettingsPageActivity内部已检查登录,这里直接跳转
|
||||
SettingsPageActivity.start(this, "");
|
||||
return;
|
||||
}
|
||||
if (action == DrawerCardItem.ACTION_HELP) {
|
||||
// 帮助页面不需要登录
|
||||
SettingsPageActivity.start(this, SettingsPageActivity.PAGE_HELP);
|
||||
return;
|
||||
}
|
||||
|
|
@ -274,6 +320,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
binding.avatarButton.setOnClickListener(v -> {
|
||||
// 检查登录状态,个人主页需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||
return;
|
||||
}
|
||||
ProfileActivity.start(this);
|
||||
finish();
|
||||
});
|
||||
|
|
@ -408,6 +458,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
return true;
|
||||
}
|
||||
if (id == R.id.nav_profile) {
|
||||
// 检查登录状态,个人主页需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||
return false;
|
||||
}
|
||||
ProfileActivity.start(this);
|
||||
finish();
|
||||
return true;
|
||||
|
|
@ -654,6 +708,32 @@ public class MainActivity extends AppCompatActivity {
|
|||
} else {
|
||||
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_LOCATION_PERMISSION) {
|
||||
// 处理位置权限请求结果
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 权限已授予,刷新附近页面显示数据
|
||||
showNearbyTab();
|
||||
} else {
|
||||
// 权限被拒绝
|
||||
// 检查是否是因为用户选择了"不再询问"
|
||||
// 如果 shouldShowRequestPermissionRationale 返回 false,说明用户可能选择了"不再询问"
|
||||
boolean shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
if (!shouldShowRationale) {
|
||||
// 用户可能选择了"不再询问",提示去设置中开启
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("需要位置权限")
|
||||
.setMessage("您已拒绝位置权限,如需使用附近功能,请在设置中手动开启位置权限。")
|
||||
.setPositiveButton("去设置", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(android.net.Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
// 不切换页面,让用户停留在附近页面
|
||||
// 界面已经显示了需要权限的提示,用户可以再次点击"授权"按钮
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -816,6 +896,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
dialog.setOnShowListener(d -> {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
// 检查登录状态,创建直播间需要登录
|
||||
if (!AuthHelper.requireLogin(this, "创建直播间需要登录")) {
|
||||
dialog.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
|
||||
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
|
||||
|
||||
|
|
@ -1398,6 +1484,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
/**
|
||||
* 初始化顶部标签页数据
|
||||
* TODO: 接入后端接口 - 获取顶部标签页配置(关注/发现/附近)
|
||||
* 接口路径: GET /api/home/tabs
|
||||
* 请求参数: 无(从token中获取userId,可选)
|
||||
* 返回数据格式: ApiResponse<List<TabConfig>>
|
||||
* TabConfig对象应包含: id, name, iconUrl, badgeCount(未读数等)等字段
|
||||
* 用于动态配置顶部标签页,支持个性化显示
|
||||
*/
|
||||
private void initializeTopTabData() {
|
||||
// 初始化关注页面数据(已关注主播的直播)
|
||||
|
|
@ -1498,16 +1590,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
* 显示附近页面
|
||||
*/
|
||||
private void showNearbyTab() {
|
||||
// 检查位置权限
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
&& ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 请求位置权限
|
||||
requestLocationPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先切换界面布局(无论是否有权限)
|
||||
// 隐藏分类标签(附近页面不需要分类筛选)
|
||||
if (binding.categoryTabs != null) {
|
||||
binding.categoryTabs.setVisibility(View.GONE);
|
||||
|
|
@ -1526,10 +1609,37 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (binding.roomsRecyclerView != null) {
|
||||
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
|
||||
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
|
||||
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
|
||||
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 检查位置权限
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
&& ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 显示需要权限的提示
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setIcon(R.drawable.ic_search_24);
|
||||
binding.emptyStateView.setTitle("需要位置权限");
|
||||
binding.emptyStateView.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播");
|
||||
binding.emptyStateView.setActionText("授权");
|
||||
binding.emptyStateView.setOnActionClickListener(v -> requestLocationPermission());
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// 清空列表,因为还没有权限获取数据
|
||||
if (nearbyUsersAdapter != null) {
|
||||
nearbyUsersAdapter.submitList(new ArrayList<>());
|
||||
}
|
||||
// 请求位置权限(但不阻塞界面切换)
|
||||
requestLocationPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
// 有权限,显示附近用户列表
|
||||
if (nearbyUsersAdapter != null) {
|
||||
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
|
||||
}
|
||||
|
||||
// 更新空状态
|
||||
if (nearbyUsers.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
|
|
@ -1708,11 +1818,18 @@ public class MainActivity extends AppCompatActivity {
|
|||
* 请求位置权限
|
||||
*/
|
||||
private void requestLocationPermission() {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
// 检查是否应该显示权限说明
|
||||
// shouldShowRequestPermissionRationale 返回 true 表示用户之前拒绝过,但还可以再次请求
|
||||
// 返回 false 可能是第一次请求,也可能是用户选择了"不再询问"
|
||||
boolean shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
if (shouldShowRationale) {
|
||||
// 用户之前拒绝过,但还可以再次请求,显示说明后直接请求权限
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("需要位置权限")
|
||||
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。")
|
||||
.setPositiveButton("授权", (dialog, which) -> {
|
||||
// 直接请求权限
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
|
|
@ -1720,19 +1837,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
},
|
||||
REQUEST_LOCATION_PERMISSION);
|
||||
})
|
||||
.setNegativeButton("取消", (dialog, which) -> {
|
||||
// 用户拒绝权限,显示提示
|
||||
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
|
||||
// 切换回发现页面
|
||||
if (binding.topTabs != null) {
|
||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
||||
if (discoverTab != null) {
|
||||
discoverTab.select();
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
} else {
|
||||
// 第一次请求权限,或者用户选择了"不再询问"
|
||||
// 直接请求权限,如果用户选择了"不再询问",系统会静默失败
|
||||
// 我们会在权限回调中处理这种情况
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,15 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void applyFilter(String query) {
|
||||
// TODO: 接入后端接口 - 搜索好友
|
||||
// 接口路径: GET /api/friends/search
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - keyword: 搜索关键词
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<FriendItem>>
|
||||
// 搜索范围包括:好友昵称、备注、共同关注等
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
updateEmptyState(all);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DiffUtil;
|
|||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.databinding.ItemNearbyUserBinding;
|
||||
|
||||
public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapter.VH> {
|
||||
|
|
@ -48,29 +49,51 @@ public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapt
|
|||
}
|
||||
|
||||
void bind(NearbyUser user) {
|
||||
binding.userName.setText(user != null && user.getName() != null ? user.getName() : "");
|
||||
if (user == null) return;
|
||||
|
||||
// 格式化距离文本
|
||||
String distanceText = user != null && user.getDistanceText() != null ? user.getDistanceText() : "";
|
||||
if (!distanceText.isEmpty() && !distanceText.startsWith("距离")) {
|
||||
binding.distanceText.setText("距离 " + distanceText);
|
||||
} else {
|
||||
binding.distanceText.setText(distanceText);
|
||||
// 设置用户名
|
||||
binding.userName.setText(user.getName() != null ? user.getName() : "");
|
||||
|
||||
// 格式化距离文本(移除"距离"前缀,因为布局中已经有位置图标)
|
||||
String distanceText = user.getDistanceText() != null ? user.getDistanceText() : "";
|
||||
if (distanceText.startsWith("距离")) {
|
||||
// 如果已经有"距离"前缀,移除它
|
||||
distanceText = distanceText.replaceFirst("距离\\s*", "");
|
||||
}
|
||||
binding.distanceText.setText(distanceText);
|
||||
|
||||
// 设置头像(使用简单的占位符,不再使用动态比例)
|
||||
// 头像已经在布局中固定为圆形,不需要动态调整
|
||||
// 使用Glide加载头像
|
||||
// TODO: 接入后端接口 - 从后端获取附近用户头像URL
|
||||
// 接口路径: GET /api/user/profile/{userId}
|
||||
// 请求参数: userId (路径参数,从NearbyUser对象中获取)
|
||||
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||
// 目前使用默认占位图
|
||||
Glide.with(binding.avatarImage.getContext())
|
||||
.load((String) null) // 暂时为null,等待后端接口
|
||||
.circleCrop()
|
||||
.placeholder(com.example.livestreaming.R.drawable.ic_account_circle_24)
|
||||
.error(com.example.livestreaming.R.drawable.ic_account_circle_24)
|
||||
.into(binding.avatarImage);
|
||||
|
||||
// 显示/隐藏直播状态徽章
|
||||
if (user.isLive()) {
|
||||
binding.liveBadge.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.liveBadge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 添加按钮点击事件
|
||||
binding.addButton.setOnClickListener(v -> {
|
||||
if (user == null) return;
|
||||
if (onUserClickListener != null) onUserClickListener.onUserClick(user);
|
||||
if (onUserClickListener != null) {
|
||||
onUserClickListener.onUserClick(user);
|
||||
}
|
||||
});
|
||||
|
||||
// 整个item点击也可以触发添加
|
||||
binding.getRoot().setOnClickListener(v -> {
|
||||
if (user == null) return;
|
||||
if (onUserClickListener != null) onUserClickListener.onUserClick(user);
|
||||
if (onUserClickListener != null) {
|
||||
onUserClickListener.onUserClick(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ public class NotificationSettingsActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void refreshItems() {
|
||||
// TODO: 接入后端接口 - 获取用户通知设置
|
||||
// 接口路径: GET /api/users/{userId}/notification/settings
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<NotificationSettings>
|
||||
// NotificationSettings对象应包含: systemEnabled, followEnabled, commentEnabled,
|
||||
// messageEnabled, liveEnabled, dndEnabled, dndStartHour, dndEndHour等字段
|
||||
// 首次加载时从接口获取,后续可从本地缓存读取
|
||||
List<MoreItem> items = new ArrayList<>();
|
||||
|
||||
// 系统通知开关
|
||||
|
|
@ -123,6 +131,20 @@ public class NotificationSettingsActivity extends AppCompatActivity {
|
|||
.setTitle("免打扰设置")
|
||||
.setMessage(message)
|
||||
.setPositiveButton("开启", (dialog, which) -> {
|
||||
// TODO: 接入后端接口 - 更新通知设置
|
||||
// 接口路径: PUT /api/users/{userId}/notification/settings
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - systemEnabled (可选): 系统通知开关
|
||||
// - followEnabled (可选): 关注提醒开关
|
||||
// - commentEnabled (可选): 评论提醒开关
|
||||
// - messageEnabled (可选): 私信提醒开关
|
||||
// - liveEnabled (可选): 开播提醒开关
|
||||
// - dndEnabled (可选): 免打扰开关
|
||||
// - dndStartHour (可选): 免打扰开始时间(小时,0-23)
|
||||
// - dndEndHour (可选): 免打扰结束时间(小时,0-23)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 更新成功后,同步更新本地缓存和界面显示
|
||||
prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply();
|
||||
refreshItems();
|
||||
Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ public class NotificationsActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 检查登录状态,通知列表需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看通知需要登录")) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ public class NotificationsAdapter extends ListAdapter<NotificationItem, Notifica
|
|||
}
|
||||
|
||||
private void loadAvatar(NotificationItem item) {
|
||||
// TODO: 接入后端接口 - 从后端获取通知发送者的头像URL
|
||||
// 接口路径: GET /api/user/profile/{senderId}
|
||||
// 请求参数: senderId (路径参数,从NotificationItem中获取)
|
||||
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||
// 如果NotificationItem包含senderId,则调用此接口获取头像;否则使用默认图标
|
||||
if (binding.avatar == null) return;
|
||||
|
||||
String avatarUrl = item.getAvatarUrl();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ public class PlayerActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
// TODO: 接入后端接口 - 记录播放开始
|
||||
// 接口路径: POST /api/play/start
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取,可选)
|
||||
// - roomId: 房间ID(从Intent中获取)
|
||||
// - playUrl: 播放地址
|
||||
// - timestamp: 开始播放时间戳
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 用于统计播放数据和用户行为分析
|
||||
String url = getIntent().getStringExtra(EXTRA_PLAY_URL);
|
||||
if (url == null || url.trim().isEmpty()) return;
|
||||
|
||||
|
|
@ -115,6 +124,15 @@ public class PlayerActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
// TODO: 接入后端接口 - 记录播放结束
|
||||
// 接口路径: POST /api/play/end
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取,可选)
|
||||
// - roomId: 房间ID(从Intent中获取)
|
||||
// - playDuration: 播放时长(秒)
|
||||
// - timestamp: 结束播放时间戳
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 用于统计播放时长和用户观看行为
|
||||
releaseExoPlayer();
|
||||
releaseIjkPlayer();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,13 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
|
||||
private void setupNavigationClicks() {
|
||||
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
|
||||
binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
|
||||
binding.topActionClock.setOnClickListener(v -> {
|
||||
// 检查登录状态,观看历史需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
|
||||
return;
|
||||
}
|
||||
WatchHistoryActivity.start(this);
|
||||
});
|
||||
binding.topActionMore.setOnClickListener(v -> TabPlaceholderActivity.start(this, "更多"));
|
||||
|
||||
binding.copyIdBtn.setOnClickListener(v -> {
|
||||
|
|
@ -267,19 +273,53 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
// - userId: 用户ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
|
||||
// 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
|
||||
binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
|
||||
binding.followers.setOnClickListener(v -> FansListActivity.start(this));
|
||||
binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
|
||||
binding.following.setOnClickListener(v -> {
|
||||
// 检查登录状态,查看关注列表需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
FollowingListActivity.start(this);
|
||||
});
|
||||
binding.followers.setOnClickListener(v -> {
|
||||
// 检查登录状态,查看粉丝列表需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看粉丝列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
FansListActivity.start(this);
|
||||
});
|
||||
binding.likes.setOnClickListener(v -> {
|
||||
// 检查登录状态,查看获赞列表需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看获赞列表需要登录")) {
|
||||
return;
|
||||
}
|
||||
LikesListActivity.start(this);
|
||||
});
|
||||
|
||||
binding.action1.setOnClickListener(v -> TabPlaceholderActivity.start(this, "公园勋章"));
|
||||
binding.action2.setOnClickListener(v -> WatchHistoryActivity.start(this));
|
||||
binding.action2.setOnClickListener(v -> {
|
||||
// 检查登录状态,观看历史需要登录
|
||||
if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
|
||||
return;
|
||||
}
|
||||
WatchHistoryActivity.start(this);
|
||||
});
|
||||
binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
|
||||
|
||||
binding.editProfile.setOnClickListener(v -> {
|
||||
// 检查登录状态,编辑资料需要登录
|
||||
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||
editProfileLauncher.launch(intent);
|
||||
});
|
||||
binding.shareHome.setOnClickListener(v -> showShareProfileDialog());
|
||||
binding.shareHome.setOnClickListener(v -> {
|
||||
// 检查登录状态,分享个人主页需要登录
|
||||
if (!AuthHelper.requireLogin(this, "分享个人主页需要登录")) {
|
||||
return;
|
||||
}
|
||||
showShareProfileDialog();
|
||||
});
|
||||
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
|
||||
}
|
||||
|
||||
|
|
@ -304,16 +344,60 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
|
||||
binding.worksPublishBtn.setOnClickListener(v -> Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show());
|
||||
// TODO: 接入后端接口 - 发布作品
|
||||
// 接口路径: POST /api/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - title: 作品标题
|
||||
// - description: 作品描述(可选)
|
||||
// - coverUrl: 封面图片URL(必填,需要先上传图片)
|
||||
// - videoUrl (可选): 视频URL(如果是视频作品)
|
||||
// - images (可选): 图片URL列表(如果是图片作品)
|
||||
// 返回数据格式: ApiResponse<WorkItem>
|
||||
// WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
|
||||
// 发布成功后,刷新作品列表显示
|
||||
binding.worksPublishBtn.setOnClickListener(v -> {
|
||||
// 检查登录状态,发布作品需要登录
|
||||
if (!AuthHelper.requireLogin(this, "发布作品需要登录")) {
|
||||
return;
|
||||
}
|
||||
Toast.makeText(this, "发布功能待接入", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
binding.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
|
||||
binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
|
||||
binding.profileEditFromTab.setOnClickListener(v -> {
|
||||
// 检查登录状态,编辑资料需要登录
|
||||
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||
editProfileLauncher.launch(intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void showTab(int index) {
|
||||
// TODO: 接入后端接口 - 获取用户作品列表
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// WorkItem对象应包含: id, title, coverUrl, likeCount, viewCount, publishTime等字段
|
||||
// TODO: 接入后端接口 - 获取用户收藏列表
|
||||
// 接口路径: GET /api/users/{userId}/favorites
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// TODO: 接入后端接口 - 获取用户赞过的作品列表
|
||||
// 接口路径: GET /api/users/{userId}/liked
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
||||
binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
|
||||
binding.tabFavorites.setVisibility(index == 1 ? View.VISIBLE : View.GONE);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,350 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.CountDownTimer;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.livestreaming.databinding.ActivityRegisterBinding;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.AuthStore;
|
||||
import com.example.livestreaming.net.LoginResponse;
|
||||
import com.example.livestreaming.net.RegisterRequest;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class RegisterActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityRegisterBinding binding;
|
||||
private boolean isRegistering = false;
|
||||
private CountDownTimer countDownTimer;
|
||||
private boolean isCodeSending = false;
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, RegisterActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityRegisterBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setupUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (countDownTimer != null) {
|
||||
countDownTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
// 返回按钮
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
// 发送验证码按钮
|
||||
binding.sendCodeButton.setOnClickListener(v -> sendVerificationCode());
|
||||
|
||||
// 注册按钮
|
||||
binding.registerButton.setOnClickListener(v -> performRegister());
|
||||
|
||||
// 登录链接
|
||||
binding.loginLinkText.setOnClickListener(v -> {
|
||||
LoginActivity.start(RegisterActivity.this);
|
||||
finish();
|
||||
});
|
||||
|
||||
// 回车键注册
|
||||
binding.nicknameInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_DONE) {
|
||||
performRegister();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void sendVerificationCode() {
|
||||
if (isCodeSending) return;
|
||||
|
||||
String phone = binding.phoneInput.getText() != null ?
|
||||
binding.phoneInput.getText().toString().trim() : "";
|
||||
|
||||
// 验证手机号
|
||||
if (TextUtils.isEmpty(phone)) {
|
||||
binding.phoneLayout.setError("请输入手机号");
|
||||
binding.phoneInput.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的手机号格式验证(11位数字)
|
||||
Pattern phonePattern = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
if (!phonePattern.matcher(phone).matches()) {
|
||||
binding.phoneLayout.setError("请输入正确的手机号");
|
||||
binding.phoneInput.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
binding.phoneLayout.setError(null);
|
||||
|
||||
// TODO: 接入后端接口 - 发送手机验证码
|
||||
// 接口路径: POST /api/sms/send
|
||||
// 请求参数:
|
||||
// - phone: 手机号
|
||||
// - type: 验证码类型(register)
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 发送成功后,开始倒计时,防止重复发送
|
||||
isCodeSending = true;
|
||||
binding.sendCodeButton.setEnabled(false);
|
||||
|
||||
// 模拟发送验证码(实际应该调用后端接口)
|
||||
Toast.makeText(this, "验证码已发送", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 开始倒计时60秒
|
||||
startCountDown(60);
|
||||
|
||||
// 实际代码应该是:
|
||||
// ApiClient.getService(getApplicationContext()).sendVerificationCode(phone, "register")
|
||||
// .enqueue(new Callback<ApiResponse<Object>>() {
|
||||
// @Override
|
||||
// public void onResponse(Call<ApiResponse<Object>> call, Response<ApiResponse<Object>> response) {
|
||||
// isCodeSending = false;
|
||||
// if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||||
// Toast.makeText(RegisterActivity.this, "验证码已发送", Toast.LENGTH_SHORT).show();
|
||||
// startCountDown(60);
|
||||
// } else {
|
||||
// binding.sendCodeButton.setEnabled(true);
|
||||
// String msg = response.body() != null ? response.body().getMessage() : "发送失败";
|
||||
// Toast.makeText(RegisterActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(Call<ApiResponse<Object>> call, Throwable t) {
|
||||
// isCodeSending = false;
|
||||
// binding.sendCodeButton.setEnabled(true);
|
||||
// Toast.makeText(RegisterActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
private void startCountDown(int seconds) {
|
||||
if (countDownTimer != null) {
|
||||
countDownTimer.cancel();
|
||||
}
|
||||
|
||||
countDownTimer = new CountDownTimer(seconds * 1000L, 1000L) {
|
||||
@Override
|
||||
public void onTick(long millisUntilFinished) {
|
||||
int remaining = (int) (millisUntilFinished / 1000);
|
||||
binding.sendCodeButton.setText(remaining + "秒后重发");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
isCodeSending = false;
|
||||
binding.sendCodeButton.setEnabled(true);
|
||||
binding.sendCodeButton.setText("发送验证码");
|
||||
}
|
||||
};
|
||||
countDownTimer.start();
|
||||
}
|
||||
|
||||
private void performRegister() {
|
||||
// 防止重复提交
|
||||
if (isRegistering) return;
|
||||
|
||||
String phone = binding.phoneInput.getText() != null ?
|
||||
binding.phoneInput.getText().toString().trim() : "";
|
||||
String verificationCode = binding.verificationCodeInput.getText() != null ?
|
||||
binding.verificationCodeInput.getText().toString().trim() : "";
|
||||
String password = binding.passwordInput.getText() != null ?
|
||||
binding.passwordInput.getText().toString().trim() : "";
|
||||
String nickname = binding.nicknameInput.getText() != null ?
|
||||
binding.nicknameInput.getText().toString().trim() : "";
|
||||
|
||||
// 验证输入
|
||||
if (TextUtils.isEmpty(phone)) {
|
||||
binding.phoneLayout.setError("请输入手机号");
|
||||
binding.phoneInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.phoneLayout.setError(null);
|
||||
}
|
||||
|
||||
Pattern phonePattern = Pattern.compile("^1[3-9]\\d{9}$");
|
||||
if (!phonePattern.matcher(phone).matches()) {
|
||||
binding.phoneLayout.setError("请输入正确的手机号");
|
||||
binding.phoneInput.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(verificationCode)) {
|
||||
binding.verificationCodeLayout.setError("请输入验证码");
|
||||
binding.verificationCodeInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.verificationCodeLayout.setError(null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.passwordLayout.setError("请输入密码");
|
||||
binding.passwordInput.requestFocus();
|
||||
return;
|
||||
} else if (password.length() < 6) {
|
||||
binding.passwordLayout.setError("密码至少6位");
|
||||
binding.passwordInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.passwordLayout.setError(null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(nickname)) {
|
||||
binding.nicknameLayout.setError("请输入昵称");
|
||||
binding.nicknameInput.requestFocus();
|
||||
return;
|
||||
} else {
|
||||
binding.nicknameLayout.setError(null);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
isRegistering = true;
|
||||
binding.registerButton.setEnabled(false);
|
||||
binding.loadingProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
// TODO: 接入后端接口 - 用户注册
|
||||
// 接口路径: POST /api/front/register(ApiService中已定义)
|
||||
// 请求参数: RegisterRequest {phone: string, password: string, verificationCode: string, nickname: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
|
||||
// 注册成功后,自动登录并保存token,更新用户信息到本地SharedPreferences
|
||||
ApiClient.getService(getApplicationContext()).register(
|
||||
new RegisterRequest(phone, password, verificationCode, nickname))
|
||||
.enqueue(new Callback<ApiResponse<LoginResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<LoginResponse>> call, Response<ApiResponse<LoginResponse>> response) {
|
||||
isRegistering = false;
|
||||
binding.registerButton.setEnabled(true);
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
|
||||
ApiResponse<LoginResponse> body = response.body();
|
||||
LoginResponse loginData = body != null ? body.getData() : null;
|
||||
|
||||
if (!response.isSuccessful() || body == null || !body.isOk() || loginData == null) {
|
||||
String errorMsg = "注册失败";
|
||||
if (body != null && !TextUtils.isEmpty(body.getMessage())) {
|
||||
errorMsg = body.getMessage();
|
||||
} else if (!response.isSuccessful()) {
|
||||
errorMsg = "网络错误:" + response.code();
|
||||
}
|
||||
Toast.makeText(RegisterActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存token
|
||||
String token = loginData.getToken();
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
AuthStore.setToken(getApplicationContext(), token);
|
||||
}
|
||||
|
||||
// 保存用户信息到本地
|
||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
||||
prefs.edit()
|
||||
.putString("profile_name", nickname)
|
||||
.apply();
|
||||
|
||||
// 注册成功,自动登录,返回上一页(如果是从其他页面跳转过来的)
|
||||
// 如果是直接打开注册页面,则跳转到主页面
|
||||
Toast.makeText(RegisterActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 检查是否有上一个Activity
|
||||
if (isTaskRoot()) {
|
||||
// 如果没有上一个Activity,跳转到主页面
|
||||
Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else {
|
||||
// 有上一个Activity,直接返回
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<LoginResponse>> call, Throwable t) {
|
||||
isRegistering = false;
|
||||
binding.registerButton.setEnabled(true);
|
||||
binding.loadingProgress.setVisibility(View.GONE);
|
||||
|
||||
// 检测是否是网络连接错误(后端未启动)
|
||||
boolean isNetworkError = false;
|
||||
String errorMsg = "网络错误";
|
||||
if (t != null) {
|
||||
String msg = t.getMessage();
|
||||
if (msg != null) {
|
||||
if (msg.contains("Unable to resolve host") ||
|
||||
msg.contains("timeout") ||
|
||||
msg.contains("Connection refused") ||
|
||||
msg.contains("Failed to connect")) {
|
||||
isNetworkError = true;
|
||||
errorMsg = "无法连接到服务器,已切换到演示模式";
|
||||
} else {
|
||||
errorMsg = "网络错误:" + msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是网络连接错误(后端未接入),使用演示模式注册
|
||||
if (isNetworkError) {
|
||||
// 演示模式:允许任意信息注册(仅用于开发测试)
|
||||
Toast.makeText(RegisterActivity.this, "演示模式:后端未接入,使用演示账号注册", Toast.LENGTH_LONG).show();
|
||||
|
||||
// 生成一个演示token(基于手机号)
|
||||
String demoToken = "demo_token_" + phone.hashCode();
|
||||
AuthStore.setToken(getApplicationContext(), demoToken);
|
||||
|
||||
// 保存用户信息到本地
|
||||
SharedPreferences prefs = getSharedPreferences("profile_prefs", MODE_PRIVATE);
|
||||
prefs.edit()
|
||||
.putString("profile_name", nickname)
|
||||
.apply();
|
||||
|
||||
// 注册成功,自动登录,返回上一页(如果是从其他页面跳转过来的)
|
||||
// 如果是直接打开注册页面,则跳转到主页面
|
||||
Toast.makeText(RegisterActivity.this, "演示模式注册成功", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 检查是否有上一个Activity
|
||||
if (isTaskRoot()) {
|
||||
// 如果没有上一个Activity,跳转到主页面
|
||||
Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else {
|
||||
// 有上一个Activity,直接返回
|
||||
finish();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(RegisterActivity.this, errorMsg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +101,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
setupUI();
|
||||
setupChat();
|
||||
|
||||
// TODO: 接入后端接口 - 记录观看历史
|
||||
// 接口路径: POST /api/watch/history
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - roomId: 房间ID
|
||||
// - watchTime: 观看时间(时间戳,可选)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 进入房间时调用,用于记录用户观看历史
|
||||
// 添加欢迎消息
|
||||
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
|
||||
}
|
||||
|
|
@ -139,6 +147,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 关注成功后,更新按钮状态为"已关注",并禁用按钮
|
||||
binding.followButton.setOnClickListener(v -> {
|
||||
// 检查登录状态,关注主播需要登录
|
||||
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
|
||||
binding.followButton.setText("已关注");
|
||||
binding.followButton.setEnabled(false);
|
||||
|
|
@ -171,6 +184,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void sendMessage() {
|
||||
// 检查登录状态,发送弹幕需要登录
|
||||
if (!AuthHelper.requireLoginWithToast(this, "发送弹幕需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 发送直播间弹幕消息
|
||||
// 接口路径: POST /api/rooms/{roomId}/messages
|
||||
// 请求参数:
|
||||
|
|
|
|||
|
|
@ -72,6 +72,19 @@ public class RoomsAdapter extends ListAdapter<Room, RoomsAdapter.RoomVH> {
|
|||
binding.roomTitle.setText(room != null && room.getTitle() != null ? room.getTitle() : "(Untitled)");
|
||||
binding.streamerName.setText(room != null && room.getStreamerName() != null ? room.getStreamerName() : "");
|
||||
|
||||
// TODO: 接入后端接口 - 获取房间点赞数
|
||||
// 接口路径: GET /api/rooms/{roomId}/likes/count
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{likeCount: number}>
|
||||
// 或者Room对象应包含likeCount字段,直接从room.getLikeCount()获取
|
||||
// TODO: 接入后端接口 - 点赞/取消点赞房间
|
||||
// 接口路径: POST /api/rooms/{roomId}/like 或 DELETE /api/rooms/{roomId}/like
|
||||
// 请求参数:
|
||||
// - roomId: 房间ID(路径参数)
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - action: "like" 或 "unlike"
|
||||
// 返回数据格式: ApiResponse<{success: boolean, likeCount: number, isLiked: boolean}>
|
||||
// 点赞成功后,更新本地点赞数和点赞状态
|
||||
try {
|
||||
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
|
||||
int h = Math.abs(seed.hashCode());
|
||||
|
|
@ -114,15 +127,28 @@ public class RoomsAdapter extends ListAdapter<Room, RoomsAdapter.RoomVH> {
|
|||
assetFile = a.coverAssetFiles.get(idx);
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 从后端获取房间封面图片URL
|
||||
// 接口路径: GET /api/rooms/{roomId}/cover
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{coverUrl: string}>
|
||||
// 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
|
||||
// 优先使用Room对象中的coverUrl,如果没有则使用默认占位图
|
||||
Object model;
|
||||
if (assetFile != null) {
|
||||
model = "file:///android_asset/img/" + assetFile;
|
||||
} else {
|
||||
// 优先从Room对象获取封面URL
|
||||
String coverUrl = room != null ? room.getCoverUrl() : null;
|
||||
if (coverUrl != null && !coverUrl.trim().isEmpty()) {
|
||||
model = coverUrl;
|
||||
} else {
|
||||
// 降级方案:使用占位图或随机图片
|
||||
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
|
||||
int h = Math.abs(seed.hashCode());
|
||||
int imageId = (h % 1000) + 1;
|
||||
model = "https://picsum.photos/id/" + imageId + "/600/450";
|
||||
}
|
||||
}
|
||||
|
||||
Glide.with(binding.coverImage)
|
||||
.load(model)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,23 @@ public class SearchActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void applyFilter(String q) {
|
||||
// TODO: 接入后端接口 - 实时搜索建议(当用户输入时)
|
||||
// 接口路径: GET /api/search/suggestions
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词(必填)
|
||||
// - limit (可选): 返回数量限制,默认10
|
||||
// 返回数据格式: ApiResponse<List<SearchSuggestion>>
|
||||
// SearchSuggestion对象应包含: type (room/user), id, title, subtitle, avatarUrl等字段
|
||||
// 用于在用户输入时显示搜索建议,提升搜索体验
|
||||
// TODO: 接入后端接口 - 搜索功能(当用户提交搜索时)
|
||||
// 接口路径: GET /api/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词(必填)
|
||||
// - type (可选): 搜索类型(room/user/all),默认all
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[], total: number}>
|
||||
// 搜索逻辑应迁移到后端,前端只负责展示结果
|
||||
String query = q != null ? q.trim() : "";
|
||||
if (query.isEmpty()) {
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ public class SearchSuggestionsAdapter extends ListAdapter<Room, SearchSuggestion
|
|||
}
|
||||
|
||||
void bind(Room room) {
|
||||
// TODO: 接入后端接口 - 从后端获取搜索建议的房间封面图片URL
|
||||
// 接口路径: GET /api/rooms/{roomId}/cover
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{coverUrl: string}>
|
||||
// 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
|
||||
// 如果搜索建议包含封面图片,使用Glide加载;否则使用默认占位图
|
||||
String title = room != null && room.getTitle() != null ? room.getTitle() : "";
|
||||
String sub = room != null && room.getStreamerName() != null ? room.getStreamerName() : "";
|
||||
binding.title.setText(title);
|
||||
|
|
|
|||
|
|
@ -45,12 +45,22 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 检查登录状态,设置页面需要登录(除了服务器设置等通用设置)
|
||||
page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||
if (page != null && !PAGE_SERVER.equals(page) && !PAGE_ABOUT.equals(page) && !PAGE_HELP.equals(page)) {
|
||||
if (!AuthHelper.requireLogin(this, "此设置需要登录")) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
binding = ActivitySettingsPageBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||
// page已在onCreate开始处获取
|
||||
if (page == null) page = "";
|
||||
|
||||
String title = resolveTitle(page);
|
||||
|
|
@ -478,6 +488,14 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showChangePasswordDialog() {
|
||||
// TODO: 接入后端接口 - 修改密码
|
||||
// 接口路径: POST /api/users/{userId}/password/change
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - oldPassword: 旧密码
|
||||
// - newPassword: 新密码
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 修改成功后,提示用户并关闭对话框
|
||||
EditText oldPassword = new EditText(this);
|
||||
oldPassword.setHint("请输入旧密码");
|
||||
oldPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
|
|
@ -503,6 +521,20 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showBindPhoneDialog() {
|
||||
// TODO: 接入后端接口 - 绑定手机号
|
||||
// 接口路径: POST /api/users/{userId}/phone/bind
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - phone: 手机号
|
||||
// - verificationCode: 验证码(需要先调用发送验证码接口)
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// TODO: 接入后端接口 - 发送手机验证码
|
||||
// 接口路径: POST /api/sms/send
|
||||
// 请求参数:
|
||||
// - phone: 手机号
|
||||
// - type: 验证码类型(bind/change等)
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 绑定成功后,提示用户并关闭对话框
|
||||
EditText phoneInput = new EditText(this);
|
||||
phoneInput.setHint("请输入手机号");
|
||||
phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
|
||||
|
|
@ -518,6 +550,19 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showDeviceManagementDialog() {
|
||||
// TODO: 接入后端接口 - 获取登录设备列表
|
||||
// 接口路径: GET /api/users/{userId}/devices
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<List<DeviceInfo>>
|
||||
// DeviceInfo对象应包含: deviceId, deviceName, deviceType, loginTime, lastActiveTime, isCurrent等字段
|
||||
// TODO: 接入后端接口 - 退出指定设备登录
|
||||
// 接口路径: DELETE /api/users/{userId}/devices/{deviceId}
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - deviceId: 设备ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 退出成功后,从设备列表中移除该设备
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("登录设备管理")
|
||||
.setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
|
||||
|
|
@ -526,6 +571,26 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showBlacklistDialog() {
|
||||
// TODO: 接入后端接口 - 获取黑名单列表
|
||||
// 接口路径: GET /api/users/{userId}/blacklist
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, addToBlacklistTime等字段
|
||||
// TODO: 接入后端接口 - 添加用户到黑名单
|
||||
// 接口路径: POST /api/users/{userId}/blacklist
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - targetUserId: 要屏蔽的用户ID
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// TODO: 接入后端接口 - 从黑名单移除用户
|
||||
// 接口路径: DELETE /api/users/{userId}/blacklist/{targetUserId}
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - targetUserId: 要解除屏蔽的用户ID(路径参数)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("黑名单管理")
|
||||
.setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
|
||||
|
|
@ -655,6 +720,18 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
"A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
|
||||
"Q7: 如何举报不良内容?\n" +
|
||||
"A: 在直播间或用户主页可以举报违规内容,我们会及时处理。\n\n" +
|
||||
// TODO: 接入后端接口 - 举报功能
|
||||
// 接口路径: POST /api/report
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - targetType: 举报类型(room/user/message/work等)
|
||||
// - targetId: 被举报对象ID
|
||||
// - reason: 举报原因(可选)
|
||||
// - description: 举报描述(可选)
|
||||
// - images (可选): 举报截图URL列表
|
||||
// 返回数据格式: ApiResponse<{success: boolean, reportId: string}>
|
||||
// 举报成功后,提示用户并记录举报信息
|
||||
//
|
||||
"更多问题请联系客服。";
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
|
|
@ -665,6 +742,15 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showFeedbackDialog() {
|
||||
// TODO: 接入后端接口 - 提交意见反馈
|
||||
// 接口路径: POST /api/feedback
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取,可选)
|
||||
// - content: 反馈内容(必填)
|
||||
// - contact: 联系方式(可选,邮箱或手机号)
|
||||
// - images (可选): 反馈图片URL列表
|
||||
// 返回数据格式: ApiResponse<{success: boolean, feedbackId: string}>
|
||||
// 提交成功后,提示用户并关闭对话框
|
||||
EditText feedbackInput = new EditText(this);
|
||||
feedbackInput.setHint("请输入您的意见或建议");
|
||||
feedbackInput.setMinLines(5);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ public class ShareUtils {
|
|||
|
||||
/**
|
||||
* 生成个人主页分享链接
|
||||
* TODO: 接入后端接口 - 获取个人主页分享链接
|
||||
* 接口路径: GET /api/share/profile/{userId}
|
||||
* 请求参数: userId (路径参数)
|
||||
* 返回数据格式: ApiResponse<{shareLink: string, shareCode: string}>
|
||||
* 后端可以生成短链接、二维码等,并记录分享统计
|
||||
*/
|
||||
public static String generateProfileShareLink(String userId) {
|
||||
return "https://livestreaming.com/profile/" + userId;
|
||||
|
|
@ -18,6 +23,11 @@ public class ShareUtils {
|
|||
|
||||
/**
|
||||
* 生成直播间分享链接
|
||||
* TODO: 接入后端接口 - 获取直播间分享链接
|
||||
* 接口路径: GET /api/share/room/{roomId}
|
||||
* 请求参数: roomId (路径参数)
|
||||
* 返回数据格式: ApiResponse<{shareLink: string, shareCode: string, qrCodeUrl: string}>
|
||||
* 后端可以生成短链接、二维码等,并记录分享统计
|
||||
*/
|
||||
public static String generateRoomShareLink(String roomId) {
|
||||
return "https://livestreaming.com/room/" + roomId;
|
||||
|
|
@ -25,6 +35,15 @@ public class ShareUtils {
|
|||
|
||||
/**
|
||||
* 分享链接(使用系统分享菜单)
|
||||
* TODO: 接入后端接口 - 记录分享行为
|
||||
* 接口路径: POST /api/share/record
|
||||
* 请求参数:
|
||||
* - userId: 当前用户ID(从token中获取,可选)
|
||||
* - shareType: 分享类型(profile/room/work等)
|
||||
* - targetId: 目标ID(用户ID/房间ID/作品ID等)
|
||||
* - sharePlatform: 分享平台(wechat/weibo/qq/system等,可选)
|
||||
* 返回数据格式: ApiResponse<{success: boolean}>
|
||||
* 用于统计分享数据和用户行为分析
|
||||
*/
|
||||
public static void shareLink(Context context, String link, String title, String text) {
|
||||
if (context == null || link == null) return;
|
||||
|
|
|
|||
|
|
@ -142,7 +142,21 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
|
||||
binding.discoverShortcutLive.setOnClickListener(v -> Toast.makeText(this, "进入直播模块", Toast.LENGTH_SHORT).show());
|
||||
binding.discoverShortcutNearby.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
|
||||
// TODO: 接入后端接口 - 获取榜单数据
|
||||
// 接口路径: GET /api/rankings
|
||||
// 请求参数:
|
||||
// - type (可选): 榜单类型(hot/new/rich等)
|
||||
// - category (可选): 分类筛选
|
||||
// 返回数据格式: ApiResponse<List<RankingItem>>
|
||||
// RankingItem对象应包含: rank, roomId, title, streamerName, viewerCount, likeCount等字段
|
||||
binding.discoverShortcutRank.setOnClickListener(v -> Toast.makeText(this, "榜单功能待接入", Toast.LENGTH_SHORT).show());
|
||||
// TODO: 接入后端接口 - 获取话题列表
|
||||
// 接口路径: GET /api/topics
|
||||
// 请求参数:
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<TopicItem>>
|
||||
// TopicItem对象应包含: id, title, coverUrl, participantCount, postCount, hotScore等字段
|
||||
binding.discoverShortcutTopic.setOnClickListener(v -> Toast.makeText(this, "话题功能待接入", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +194,13 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void applyDiscoverFilter(String q) {
|
||||
// TODO: 接入后端接口 - 发现页搜索建议
|
||||
// 接口路径: GET /api/discover/search/suggestions
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词(必填)
|
||||
// - limit (可选): 返回数量限制,默认10
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// 用于在发现页搜索框输入时显示实时搜索建议
|
||||
String query = q != null ? q.trim() : "";
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
|
||||
|
|
@ -227,6 +248,13 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
|
||||
ensureFollowRoomsAdapter();
|
||||
|
||||
// TODO: 接入后端接口 - 获取关注主播的直播间列表(TabPlaceholderActivity中的关注页面)
|
||||
// 接口路径: GET /api/following/rooms
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// 初始化数据
|
||||
followAllRooms.clear();
|
||||
followAllRooms.addAll(buildFollowDemoRooms(16));
|
||||
|
|
@ -327,6 +355,14 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.nearbyRecyclerView.setLayoutManager(layoutManager);
|
||||
binding.nearbyRecyclerView.setAdapter(adapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取附近用户列表(TabPlaceholderActivity中的附近页面)
|
||||
// 接口路径: GET /api/users/nearby
|
||||
// 请求参数:
|
||||
// - latitude: 当前用户纬度(必填)
|
||||
// - longitude: 当前用户经度(必填)
|
||||
// - radius (可选): 搜索半径(单位:米,默认5000)
|
||||
// - page (可选): 页码
|
||||
// 返回数据格式: ApiResponse<List<NearbyUser>>
|
||||
adapter.submitList(buildNearbyDemoUsers(18));
|
||||
}
|
||||
|
||||
|
|
@ -407,6 +443,12 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.parkBadgeRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||
binding.parkBadgeRecyclerView.setAdapter(badgesAdapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取用户勋章列表
|
||||
// 接口路径: GET /api/users/{userId}/badges
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// 返回数据格式: ApiResponse<List<BadgeItem>>
|
||||
// BadgeItem对象应包含: id, name, desc, iconUrl, isAchieved, isLocked, achieveTime等字段
|
||||
badgesAdapter.submitList(buildDemoBadges());
|
||||
}
|
||||
|
||||
|
|
@ -548,14 +590,21 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showLogoutConfirm() {
|
||||
// TODO: 接入后端接口 - 退出登录
|
||||
// 接口路径: POST /api/logout 或 GET /api/logout
|
||||
// 请求参数: 无(从token中获取userId)
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 退出成功后,清除本地token和用户信息,跳转到登录页面
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("退出登录")
|
||||
.setMessage("确定要退出登录吗?")
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("退出", (d, w) -> {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
// 清除token
|
||||
com.example.livestreaming.net.AuthStore.setToken(getApplicationContext(), null);
|
||||
|
||||
// 跳转到登录页面
|
||||
LoginActivity.start(TabPlaceholderActivity.this);
|
||||
finish();
|
||||
})
|
||||
.show();
|
||||
|
|
@ -572,9 +621,34 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
// TODO: 接入后端接口 - 刷新定位并获取附近数据
|
||||
// 接口路径: POST /api/users/{userId}/location/update
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(从token中获取)
|
||||
// - latitude: 当前用户纬度
|
||||
// - longitude: 当前用户经度
|
||||
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||
// 更新成功后,重新获取附近用户、附近直播、热门地点等数据
|
||||
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
|
||||
binding.nearbyEntryUsers.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
|
||||
// TODO: 接入后端接口 - 获取附近直播列表
|
||||
// 接口路径: GET /api/rooms/nearby
|
||||
// 请求参数:
|
||||
// - latitude: 当前用户纬度(必填)
|
||||
// - longitude: 当前用户经度(必填)
|
||||
// - radius (可选): 搜索半径(单位:米,默认10000)
|
||||
// - page (可选): 页码
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// 返回距离用户最近的正在直播的房间列表
|
||||
binding.nearbyEntryLive.setOnClickListener(v -> Toast.makeText(this, "附近直播(待接入)", Toast.LENGTH_SHORT).show());
|
||||
// TODO: 接入后端接口 - 获取热门地点列表
|
||||
// 接口路径: GET /api/locations/hot
|
||||
// 请求参数:
|
||||
// - latitude: 当前用户纬度(可选,用于排序)
|
||||
// - longitude: 当前用户经度(可选,用于排序)
|
||||
// - page (可选): 页码
|
||||
// 返回数据格式: ApiResponse<List<LocationItem>>
|
||||
// LocationItem对象应包含: id, name, address, latitude, longitude, distance, liveCount, userCount等字段
|
||||
binding.nearbyEntryPlaces.setOnClickListener(v -> Toast.makeText(this, "热门地点(待接入)", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
|
|
@ -597,12 +671,36 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
|
||||
addFriendAdapter = new NearbyUsersAdapter(user -> {
|
||||
if (user == null) return;
|
||||
|
||||
// 检查登录状态,加好友需要登录
|
||||
if (!AuthHelper.requireLogin(this, "加好友需要登录")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口 - 发送好友请求
|
||||
// 接口路径: POST /api/friends/request
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - targetUserId: 目标用户ID
|
||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||
// 发送成功后,提示用户并更新按钮状态
|
||||
Toast.makeText(this, "已发送好友请求:" + user.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
binding.addFriendRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.addFriendRecyclerView.setAdapter(addFriendAdapter);
|
||||
|
||||
// TODO: 接入后端接口 - 获取推荐好友列表(加好友页面)
|
||||
// 接口路径: GET /api/friends/recommend
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - latitude (可选): 当前用户纬度(用于推荐附近的人)
|
||||
// - longitude (可选): 当前用户经度(用于推荐附近的人)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<User>>
|
||||
// User对象应包含: id, name, avatarUrl, bio, location, distance, mutualFriendsCount等字段
|
||||
// 推荐算法:基于共同好友、地理位置、兴趣标签等
|
||||
addFriendAllUsers.clear();
|
||||
addFriendAllUsers.addAll(buildNearbyDemoUsers(18));
|
||||
addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ public class UnreadMessageManager {
|
|||
|
||||
/**
|
||||
* 获取总未读消息数量
|
||||
* TODO: 接入后端接口 - 从后端获取未读消息总数
|
||||
* 接口路径: GET /api/messages/unread/count
|
||||
* 请求参数: 无(从token中获取userId)
|
||||
* 返回数据格式: ApiResponse<{unreadCount: number}>
|
||||
* 建议:在应用启动时和进入后台后重新进入时调用此接口更新未读数量
|
||||
*/
|
||||
public static int getUnreadCount(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
|
|
|||
|
|
@ -132,6 +132,27 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showTab(int index) {
|
||||
// TODO: 接入后端接口 - 获取其他用户的作品列表
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// TODO: 接入后端接口 - 获取其他用户的收藏列表
|
||||
// 接口路径: GET /api/users/{userId}/favorites
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// TODO: 接入后端接口 - 获取其他用户赞过的作品列表
|
||||
// 接口路径: GET /api/users/{userId}/liked
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
if (binding == null) return;
|
||||
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
||||
binding.worksRecycler.setVisibility(index == 0 ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ public class UserWorksAdapter extends RecyclerView.Adapter<UserWorksAdapter.VH>
|
|||
|
||||
private final List<Integer> items = new ArrayList<>();
|
||||
|
||||
// TODO: 接入后端接口 - 加载用户作品数据
|
||||
// 接口路径: GET /api/users/{userId}/works
|
||||
// 请求参数:
|
||||
// - userId: 用户ID(路径参数)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<WorkItem>>
|
||||
// WorkItem对象应包含: id, coverUrl, title, likeCount, viewCount, publishTime等字段
|
||||
// 注意:当前使用Integer列表(drawable资源ID)作为临时方案,应改为使用WorkItem对象列表
|
||||
public void submitList(List<Integer> list) {
|
||||
items.clear();
|
||||
if (list != null) items.addAll(list);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,20 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
|
|||
}
|
||||
|
||||
void bind(Room room) {
|
||||
// TODO: 接入后端接口 - 从后端获取房间封面图片URL
|
||||
// 接口路径: GET /api/rooms/{roomId}/cover
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{coverUrl: string}>
|
||||
// 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
|
||||
// TODO: 接入后端接口 - 从后端获取主播头像URL
|
||||
// 接口路径: GET /api/user/profile/{streamerId}
|
||||
// 请求参数: streamerId (路径参数,从Room对象中获取streamerId)
|
||||
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||
// TODO: 接入后端接口 - 获取房间观看人数
|
||||
// 接口路径: GET /api/rooms/{roomId}/viewers/count
|
||||
// 请求参数: roomId (路径参数)
|
||||
// 返回数据格式: ApiResponse<{viewerCount: number}>
|
||||
// 或者Room对象应包含viewerCount字段,直接从room.getViewerCount()获取
|
||||
if (room == null) return;
|
||||
|
||||
// 设置标题
|
||||
|
|
|
|||
|
|
@ -92,6 +92,13 @@ public class WishTreeActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// TODO: 接入后端接口 - 获取愿望树相关数据
|
||||
// 接口路径: GET /api/wish_tree/info
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取,可选)
|
||||
// 返回数据格式: ApiResponse<WishTreeInfo>
|
||||
// WishTreeInfo对象应包含: totalWishes, todayWishes, userWishCount, nextResetTime等字段
|
||||
// 用于显示愿望树统计信息和倒计时
|
||||
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ public interface ApiService {
|
|||
@POST("api/front/login")
|
||||
Call<ApiResponse<LoginResponse>> login(@Body LoginRequest body);
|
||||
|
||||
@POST("api/front/register")
|
||||
Call<ApiResponse<LoginResponse>> register(@Body RegisterRequest body);
|
||||
|
||||
@GET("api/rooms")
|
||||
Call<ApiResponse<List<Room>>> getRooms();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class RegisterRequest {
|
||||
|
||||
@SerializedName("phone")
|
||||
private final String phone;
|
||||
|
||||
@SerializedName("password")
|
||||
private final String password;
|
||||
|
||||
@SerializedName("verificationCode")
|
||||
private final String verificationCode;
|
||||
|
||||
@SerializedName("nickname")
|
||||
private final String nickname;
|
||||
|
||||
public RegisterRequest(String phone, String password, String verificationCode, String nickname) {
|
||||
this.phone = phone;
|
||||
this.password = password;
|
||||
this.verificationCode = verificationCode;
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getVerificationCode() {
|
||||
return verificationCode;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +27,9 @@ public class Room {
|
|||
@SerializedName("viewerCount")
|
||||
private int viewerCount;
|
||||
|
||||
@SerializedName("coverUrl")
|
||||
private String coverUrl;
|
||||
|
||||
@SerializedName("streamUrls")
|
||||
private StreamUrls streamUrls;
|
||||
|
||||
|
|
@ -72,6 +75,10 @@ public class Room {
|
|||
this.viewerCount = viewerCount;
|
||||
}
|
||||
|
||||
public void setCoverUrl(String coverUrl) {
|
||||
this.coverUrl = coverUrl;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
@ -104,6 +111,10 @@ public class Room {
|
|||
return viewerCount;
|
||||
}
|
||||
|
||||
public String getCoverUrl() {
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
@ -116,11 +127,12 @@ public class Room {
|
|||
&& Objects.equals(streamerName, room.streamerName)
|
||||
&& Objects.equals(type, room.type)
|
||||
&& Objects.equals(streamKey, room.streamKey)
|
||||
&& Objects.equals(coverUrl, room.coverUrl)
|
||||
&& Objects.equals(streamUrls, room.streamUrls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, streamUrls);
|
||||
return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, coverUrl, streamUrls);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#7A3FF2" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#6A2FE2" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#7A3FF2" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
6
android-app/app/src/main/res/drawable/bg_live_badge.xml
Normal file
6
android-app/app/src/main/res/drawable/bg_live_badge.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#E53935" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#0A000000" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:bottom="2dp"
|
||||
android:left="0dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
201
android-app/app/src/main/res/layout/activity_login.xml
Normal file
201
android-app/app/src/main/res/layout/activity_login.xml
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F6F7FB">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="48dp"
|
||||
android:paddingBottom="48dp">
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImageView
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="返回"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_arrow_back_24"
|
||||
android:scaleType="fitCenter"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Logo或标题区域 -->
|
||||
<TextView
|
||||
android:id="@+id/appTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="直播应用"
|
||||
android:textColor="#111111"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- 演示模式提示 -->
|
||||
<TextView
|
||||
android:id="@+id/demoModeHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="提示:后端未接入时,将使用演示模式登录"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="12sp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="欢迎回来"
|
||||
android:textColor="#666666"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/demoModeHint" />
|
||||
|
||||
<!-- 账号输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/accountLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:hint="手机号/账号"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/welcomeText"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/accountInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="密码"
|
||||
app:endIconMode="password_toggle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountLayout"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 忘记密码 -->
|
||||
<TextView
|
||||
android:id="@+id/forgotPasswordText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:text="忘记密码?"
|
||||
android:textColor="#6200EE"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordLayout" />
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/loginButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:backgroundTint="#6200EE"
|
||||
android:text="登录"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/forgotPasswordText" />
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<TextView
|
||||
android:id="@+id/registerLinkText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:text="还没有账号?立即注册"
|
||||
android:textColor="#6200EE"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginButton" />
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
261
android-app/app/src/main/res/layout/activity_register.xml
Normal file
261
android-app/app/src/main/res/layout/activity_register.xml
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F6F7FB">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="48dp"
|
||||
android:paddingBottom="48dp">
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImageView
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="返回"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_arrow_back_24"
|
||||
android:scaleType="fitCenter"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 标题 -->
|
||||
<TextView
|
||||
android:id="@+id/appTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="创建账号"
|
||||
android:textColor="#111111"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/backButton" />
|
||||
|
||||
<!-- 演示模式提示 -->
|
||||
<TextView
|
||||
android:id="@+id/demoModeHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="提示:后端未接入时,将使用演示模式注册"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="12sp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="填写信息完成注册"
|
||||
android:textColor="#666666"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/demoModeHint" />
|
||||
|
||||
<!-- 手机号输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/phoneLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:hint="手机号"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/welcomeText"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/phoneInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="phone"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/verificationCodeLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="验证码"
|
||||
app:layout_constraintEnd_toStartOf="@id/sendCodeButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/phoneLayout"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/verificationCodeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 发送验证码按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sendCodeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:backgroundTint="#6200EE"
|
||||
android:text="发送验证码"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
app:cornerRadius="12dp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/verificationCodeLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/verificationCodeLayout" />
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="密码"
|
||||
app:endIconMode="password_toggle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/verificationCodeLayout"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 昵称输入框 -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nicknameLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="昵称"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordLayout"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxStrokeColor="#E0E0E0"
|
||||
app:hintTextColor="#999999">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/nicknameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:minHeight="56dp"
|
||||
android:textColor="#111111"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:backgroundTint="#6200EE"
|
||||
android:text="注册"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:cornerRadius="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/nicknameLayout" />
|
||||
|
||||
<!-- 登录链接 -->
|
||||
<TextView
|
||||
android:id="@+id/loginLinkText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:text="已有账号?立即登录"
|
||||
android:textColor="#6200EE"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/registerButton" />
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<ProgressBar
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
|
@ -1,78 +1,127 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:background="@android:color/white"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/bg_nearby_user_card_shadow"
|
||||
android:elevation="2dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 头像容器,带直播状态指示 -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/avatarContainer"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatarImage"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_avatar_circle"
|
||||
android:padding="6dp"
|
||||
android:padding="3dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_account_circle_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 直播状态徽章 -->
|
||||
<TextView
|
||||
android:id="@+id/userName"
|
||||
android:id="@+id/liveBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:background="@drawable/bg_live_badge"
|
||||
android:gravity="center"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:text="LIVE"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatarImage"
|
||||
app:layout_constraintEnd_toEndOf="@id/avatarImage"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- 用户信息容器 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/userInfoContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatarContainer"
|
||||
app:layout_constraintEnd_toStartOf="@id/addButton"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarContainer"
|
||||
app:layout_constraintTop_toTopOf="@id/avatarContainer">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="用户昵称"
|
||||
android:textColor="#111111"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@id/addButton"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarImage"
|
||||
app:layout_constraintTop_toTopOf="@id/avatarImage" />
|
||||
android:textColor="#1A1A1A"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/locationIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:src="@android:drawable/ic_menu_mylocation"
|
||||
android:tint="#999999"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/distanceText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="距离 1.2km"
|
||||
android:textColor="#666666"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/addButton"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarImage"
|
||||
app:layout_constraintTop_toBottomOf="@id/userName" />
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<TextView
|
||||
android:id="@+id/addButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/bg_purple_999"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/bg_add_button_nearby_pressed"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:minWidth="72dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="添加"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatarContainer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="#0F000000"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/avatarImage" />
|
||||
app:layout_constraintTop_toTopOf="@id/avatarContainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
## ✅ 已完善功能
|
||||
|
||||
### 1. 主界面 (MainActivity)
|
||||
**完成度**: 90%
|
||||
**完成度**: 95%
|
||||
|
||||
- ✅ 瀑布流布局展示直播间
|
||||
- ✅ 下拉刷新
|
||||
|
|
@ -50,16 +50,19 @@
|
|||
- ✅ 推流地址显示和复制
|
||||
- ✅ 搜索框UI
|
||||
- ✅ 语音搜索功能
|
||||
- ✅ 分类标签UI
|
||||
- ✅ 分类标签UI和筛选功能(CategoryFilterManager)
|
||||
- ✅ 顶部标签页功能(关注/发现/附近)
|
||||
- ✅ 底部导航栏
|
||||
- ✅ 侧边栏菜单
|
||||
- ✅ 空状态和错误处理
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 分类筛选功能(UI已准备好,但筛选逻辑使用演示数据)
|
||||
- ⚠️ 顶部标签页(关注/发现/附近)跳转到占位页面
|
||||
- ⚠️ 分类筛选使用本地数据筛选(等待后端API)
|
||||
- ⚠️ 顶部标签页数据使用演示数据(关注列表、发现推荐、附近用户)
|
||||
- ⚠️ 附近用户功能需要位置权限,数据为模拟数据
|
||||
|
||||
### 2. 直播间详情 (RoomDetailActivity)
|
||||
**完成度**: 85%
|
||||
**完成度**: 90%
|
||||
|
||||
- ✅ 直播流播放(HLS/FLV)
|
||||
- ✅ 全屏播放
|
||||
|
|
@ -69,12 +72,13 @@
|
|||
- ✅ 观看人数显示
|
||||
- ✅ 自动重连机制
|
||||
- ✅ 低延迟播放配置
|
||||
- ✅ 分享功能(系统分享菜单)
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 聊天功能使用模拟数据,未连接真实WebSocket
|
||||
- ⚠️ 礼物打赏功能未实现
|
||||
- ⚠️ 分享功能未实现
|
||||
- ⚠️ 弹幕功能未实现
|
||||
- ⚠️ 分享链接为模拟数据(等待后端生成真实分享链接)
|
||||
|
||||
### 3. 个人资料 (ProfileActivity)
|
||||
**完成度**: 90%
|
||||
|
|
@ -86,11 +90,13 @@
|
|||
- ✅ 作品/收藏/赞过标签页
|
||||
- ✅ 关注/粉丝/获赞统计
|
||||
- ✅ 主页链接复制
|
||||
- ✅ 作品列表基础UI(UserWorksAdapter)
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 作品发布功能(显示"待接入")
|
||||
- ⚠️ 头像上传功能(仅支持本地资源)
|
||||
- ⚠️ 作品列表使用演示数据
|
||||
- ⚠️ 作品列表使用演示数据(Integer列表)
|
||||
- ⚠️ 作品详情页面未实现
|
||||
|
||||
### 4. 编辑资料 (EditProfileActivity)
|
||||
**完成度**: 95%
|
||||
|
|
@ -108,45 +114,53 @@
|
|||
- ⚠️ 数据同步到后端API
|
||||
|
||||
### 5. 消息功能 (MessagesActivity)
|
||||
**完成度**: 80%
|
||||
**完成度**: 85%
|
||||
|
||||
- ✅ 会话列表展示
|
||||
- ✅ 未读消息徽章
|
||||
- ✅ 滑动删除/标记已读
|
||||
- ✅ 会话详情页面
|
||||
- ✅ 消息发送(本地)
|
||||
- ✅ 会话搜索功能(按会话标题和消息内容搜索)
|
||||
- ✅ 空状态显示
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 所有消息数据使用演示数据
|
||||
- ⚠️ 未连接真实消息服务器
|
||||
- ⚠️ 未连接真实消息服务器(WebSocket)
|
||||
- ⚠️ 消息推送功能未实现
|
||||
- ⚠️ 图片/语音消息未实现
|
||||
|
||||
### 6. 会话详情 (ConversationActivity)
|
||||
**完成度**: 75%
|
||||
**完成度**: 90%
|
||||
|
||||
- ✅ 消息列表展示
|
||||
- ✅ 消息发送
|
||||
- ✅ 自动滚动到底部
|
||||
- ✅ 自动滚动到底部(使用smoothScrollToPosition)
|
||||
- ✅ 未读消息标记
|
||||
- ✅ 消息状态显示(发送中、已发送、已读)
|
||||
- ✅ 消息长按菜单(复制、删除)
|
||||
- ✅ 内存泄漏防护(onDestroy中清理Handler)
|
||||
- ✅ DiffUtil使用messageId优化性能
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 消息数据使用演示数据
|
||||
- ⚠️ 未实现实时消息接收
|
||||
- ⚠️ 消息类型单一(仅文本)
|
||||
- ⚠️ 未实现实时消息接收(WebSocket)
|
||||
- ⚠️ 消息类型单一(仅文本,图片/语音消息未实现)
|
||||
|
||||
### 7. 搜索功能 (SearchActivity)
|
||||
**完成度**: 60%
|
||||
**完成度**: 70%
|
||||
|
||||
- ✅ 搜索界面UI
|
||||
- ✅ 实时搜索过滤
|
||||
- ✅ 搜索结果展示
|
||||
- ✅ 空状态显示(无搜索结果时显示提示)
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 仅支持本地数据过滤
|
||||
- ⚠️ 未连接后端搜索API
|
||||
- ⚠️ 搜索历史未实现
|
||||
- ⚠️ 热门搜索未实现
|
||||
- ⚠️ 搜索建议(自动补全)未实现
|
||||
|
||||
### 8. 缘池功能 (FishPondActivity)
|
||||
**完成度**: 85%
|
||||
|
|
@ -174,42 +188,47 @@
|
|||
|
||||
### 10. 其他已实现页面
|
||||
- ✅ 观看历史 (WatchHistoryActivity) - 基础UI
|
||||
- ✅ 我的好友 (MyFriendsActivity) - 基础UI
|
||||
- ✅ 我的好友 (MyFriendsActivity) - 基础UI,支持空状态
|
||||
- ✅ 粉丝列表 (FansListActivity) - 基础UI
|
||||
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
||||
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
||||
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
|
||||
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
||||
- ✅ 登录页面 (LoginActivity) - 完整实现
|
||||
- ✅ 注册页面 (RegisterActivity) - 完整实现
|
||||
- ✅ 通知列表 (NotificationsActivity) - 完整实现,支持分类筛选
|
||||
- ✅ 通知设置 (NotificationSettingsActivity) - 完整实现
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 部分完善功能
|
||||
|
||||
### 1. 网络层集成
|
||||
**完成度**: 60%
|
||||
**完成度**: 70%
|
||||
|
||||
- ✅ Retrofit + OkHttp 配置完成
|
||||
- ✅ API 接口定义完成
|
||||
- ✅ 基础错误处理
|
||||
- ✅ 基础错误处理(ErrorHandler工具类)
|
||||
- ✅ 模拟器/真机网络地址配置
|
||||
- ✅ 网络请求生命周期管理(NetworkRequestManager)
|
||||
- ✅ 统一错误提示和重试机制
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ API 调用失败时大量使用演示数据
|
||||
- ⚠️ 缺少统一的错误处理机制
|
||||
- ⚠️ 缺少请求重试机制
|
||||
- ⚠️ 缺少网络状态监听
|
||||
- ⚠️ 缺少请求缓存策略
|
||||
|
||||
### 2. 数据持久化
|
||||
**完成度**: 50%
|
||||
### 2. 数据存储(前端)
|
||||
**完成度**: 60%
|
||||
|
||||
- ✅ SharedPreferences 用于个人资料
|
||||
- ✅ 本地数据缓存
|
||||
- ✅ SharedPreferences 用于个人资料和配置
|
||||
- ✅ 本地数据缓存(内存缓存)
|
||||
- ✅ 分类筛选条件记忆(SharedPreferences)
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 未使用 Room 数据库
|
||||
- ⚠️ 未实现离线数据支持
|
||||
- ⚠️ 未实现数据同步机制
|
||||
- ⚠️ 未实现离线数据支持(当前仅使用内存缓存)
|
||||
- ⚠️ 未实现数据同步机制(等待后端)
|
||||
|
||||
### 3. 图片处理
|
||||
**完成度**: 70%
|
||||
|
|
@ -230,12 +249,12 @@
|
|||
### 1. 后端API集成
|
||||
**问题**: 虽然定义了API接口,但很多功能在API失败时回退到演示数据
|
||||
|
||||
**需要完善**:
|
||||
- [ ] 完善所有API接口的错误处理
|
||||
- [ ] 实现API请求的加载状态管理
|
||||
- [ ] 实现API数据的本地缓存
|
||||
- [ ] 实现数据同步机制
|
||||
- [ ] 添加API版本管理
|
||||
**需要完善**(前端部分):
|
||||
- [ ] 完善所有API接口的错误处理(前端错误提示)
|
||||
- [ ] 实现API请求的加载状态管理(前端UI)
|
||||
- [ ] 添加API版本管理(前端配置)
|
||||
|
||||
**注意**: API数据的本地缓存和数据同步机制需要等待后端支持,前端仅负责UI展示和错误处理。
|
||||
|
||||
### 2. 实时通信
|
||||
**问题**: 聊天、弹幕等功能使用模拟数据
|
||||
|
|
@ -276,20 +295,30 @@
|
|||
- [ ] 添加位置权限处理
|
||||
|
||||
### 6. 占位页面功能
|
||||
**以下功能跳转到 `TabPlaceholderActivity`,需要实现**:
|
||||
**状态**: 部分功能已实现基础UI,但核心功能未实现
|
||||
|
||||
- [ ] 语音匹配 (VoiceMatchActivity)
|
||||
- [ ] 心动信号 (HeartbeatSignalActivity)
|
||||
- [ ] 在线处对象 (OnlineDatingActivity)
|
||||
- [ ] 找人玩游戏 (FindGameActivity)
|
||||
- [ ] 一起KTV (KTVTogetherActivity)
|
||||
- [ ] 你画我猜 (DrawGuessActivity)
|
||||
- [ ] 和平精英 (PeaceEliteActivity)
|
||||
- [ ] 桌子游 (TableGamesActivity)
|
||||
- [ ] 公园勋章
|
||||
- [ ] 分享主页(部分实现,仅复制链接)
|
||||
- [ ] 加好友
|
||||
- [ ] 定位/发现
|
||||
**已实现基础UI的Activity**:
|
||||
- ✅ 语音匹配 (VoiceMatchActivity) - 基础页面
|
||||
- ✅ 心动信号 (HeartbeatSignalActivity) - 基础页面
|
||||
- ✅ 在线处对象 (OnlineDatingActivity) - 基础页面
|
||||
- ✅ 找人玩游戏 (FindGameActivity) - 基础页面
|
||||
- ✅ 一起KTV (KTVTogetherActivity) - 基础页面
|
||||
- ✅ 你画我猜 (DrawGuessActivity) - 基础页面
|
||||
- ✅ 和平精英 (PeaceEliteActivity) - 基础页面
|
||||
- ✅ 桌子游 (TableGamesActivity) - 基础页面
|
||||
|
||||
**TabPlaceholderActivity中的占位功能**:
|
||||
- ⚠️ 公园勋章 - 显示占位页面
|
||||
- ⚠️ 加好友 - 显示附近用户列表(演示数据)
|
||||
- ⚠️ 定位/发现 - 显示占位页面
|
||||
- ⚠️ 关注页面(TabPlaceholderActivity中) - 显示关注列表(演示数据)
|
||||
- ⚠️ 附近页面(TabPlaceholderActivity中) - 显示附近用户(演示数据)
|
||||
- ⚠️ 发现页面(TabPlaceholderActivity中) - 显示推荐内容(演示数据)
|
||||
|
||||
**需要完善**:
|
||||
- [ ] 实现各功能的核心逻辑
|
||||
- [ ] 接入后端API获取真实数据
|
||||
- [ ] 实现游戏匹配、语音匹配等功能
|
||||
|
||||
### 7. 作品功能
|
||||
**问题**: 个人资料中的"作品"标签页未实现
|
||||
|
|
@ -384,11 +413,12 @@
|
|||
- [ ] 引入依赖注入框架(Hilt/Dagger)
|
||||
- [ ] 模块化拆分
|
||||
|
||||
### 2. 数据层优化
|
||||
- [ ] 引入 Room 数据库
|
||||
- [ ] 实现 Repository 模式
|
||||
- [ ] 添加数据同步机制
|
||||
- [ ] 实现离线数据支持
|
||||
### 2. 数据层优化(前端)
|
||||
- [ ] 实现 Repository 模式(本地数据源)
|
||||
- [ ] 优化内存缓存策略
|
||||
- [ ] 实现请求去重机制
|
||||
|
||||
**注意**: Room数据库和离线数据支持暂不实现,当前使用SharedPreferences和内存缓存。
|
||||
|
||||
### 3. UI/UX 优化
|
||||
- [ ] 统一设计语言和组件库
|
||||
|
|
@ -439,48 +469,29 @@
|
|||
- [x] 修复动画未取消问题
|
||||
- [x] 完善网络请求取消机制
|
||||
- [x] 优化播放器资源释放
|
||||
- [x] 添加 LeakCanary 检测
|
||||
|
||||
**实际工作量**: 1.5天
|
||||
**完成内容**:
|
||||
- 为所有使用Handler的Activity添加onDestroy方法,清理Runnable防止内存泄漏
|
||||
- 完善动画生命周期管理,确保动画在Activity销毁时正确取消
|
||||
- 创建NetworkRequestManager和NetworkUtils,实现生命周期感知的网络请求管理
|
||||
- 创建`NetworkRequestManager`工具类,实现生命周期感知的网络请求管理,在Activity销毁时自动取消所有请求
|
||||
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
||||
- 集成LeakCanary内存泄漏检测工具
|
||||
|
||||
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: 提升用户体验一致性
|
||||
|
||||
- [x] 创建统一的加载状态组件
|
||||
- [x] 创建统一的加载状态管理器
|
||||
- [x] 实现骨架屏(Skeleton Screen)
|
||||
- [x] 统一所有页面的加载提示
|
||||
- [x] 添加加载动画
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
|
||||
- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
|
||||
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
|
||||
- 更新了所有主要Activity使用统一的加载状态:
|
||||
- **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
|
||||
- **RoomDetailActivity**: 使用LoadingView显示加载状态
|
||||
- **SearchActivity**: 搜索时显示"搜索中..."加载状态
|
||||
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持骨架屏、下拉刷新等)
|
||||
- 实现了骨架屏适配器(`SkeletonAdapter`),在RecyclerView中显示占位效果
|
||||
- 更新了主要Activity使用统一的加载状态:
|
||||
- **MainActivity**: 在列表为空时使用骨架屏显示加载状态
|
||||
- **SearchActivity**: 搜索时显示加载状态
|
||||
- **MessagesActivity**: 加载消息列表时显示加载状态
|
||||
- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
|
||||
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
|
||||
|
||||
#### 4. **数据持久化(本地)** ⭐⭐⭐
|
||||
**为什么重要**: 支持离线使用,提升用户体验
|
||||
|
||||
- [ ] 引入 Room 数据库
|
||||
- [ ] 缓存直播间列表到本地
|
||||
- [ ] 缓存用户信息
|
||||
- [ ] 实现搜索历史存储
|
||||
- [ ] 实现观看历史存储
|
||||
- [ ] 实现消息记录缓存
|
||||
|
||||
**预计工作量**: 3-5天
|
||||
|
||||
---
|
||||
|
||||
### 🟡 第二优先级(功能完善与架构优化)
|
||||
|
|
@ -501,7 +512,7 @@
|
|||
|
||||
- [x] 完善本地数据筛选逻辑
|
||||
- [x] 实现筛选条件记忆(SharedPreferences)
|
||||
- [x] 优化筛选性能
|
||||
- [x] 优化筛选性能(异步筛选)
|
||||
- [x] 添加筛选动画效果
|
||||
|
||||
**完成内容**:
|
||||
|
|
@ -511,55 +522,88 @@
|
|||
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
|
||||
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
|
||||
- 在Activity恢复时自动恢复上次选中的分类标签
|
||||
- 支持在关注、发现、附近三个标签页中分别筛选
|
||||
|
||||
**预计工作量**: 1-2天
|
||||
**待完善**:
|
||||
- ⚠️ 等待后端API支持按分类获取房间列表
|
||||
|
||||
#### 7. **顶部标签页功能(前端实现)** ⭐⭐
|
||||
**预计工作量**: 1-2天(已完成)
|
||||
|
||||
#### 7. **顶部标签页功能(前端实现)** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: 完善主界面功能
|
||||
|
||||
- [ ] 实现关注页面(使用本地数据)
|
||||
- [ ] 实现发现页面(推荐算法前端实现)
|
||||
- [ ] 实现附近页面(使用模拟位置数据)
|
||||
- [ ] 添加位置权限申请(即使后端未就绪)
|
||||
- [x] 实现关注页面(使用本地数据)
|
||||
- [x] 实现发现页面(推荐算法前端实现)
|
||||
- [x] 实现附近页面(使用模拟位置数据)
|
||||
- [x] 添加位置权限申请(即使后端未就绪)
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
**完成内容**:
|
||||
- 实现了 `showFollowTab()`、`showDiscoverTab()`、`showNearbyTab()` 三个方法
|
||||
- 创建了 `NearbyUser` 数据模型和 `NearbyUsersAdapter` 适配器
|
||||
- 创建了 `LocationDataManager` 位置数据管理器
|
||||
- 实现了位置权限申请和权限处理逻辑
|
||||
- 关注页面显示已关注主播的直播列表
|
||||
- 发现页面显示推荐内容,支持分类筛选
|
||||
- 附近页面显示附近用户列表,支持空状态显示
|
||||
- 所有页面都支持搜索功能(在对应数据源中搜索)
|
||||
|
||||
#### 8. **搜索功能增强** ⭐
|
||||
**待完善**:
|
||||
- ⚠️ 数据使用演示数据,等待后端API
|
||||
- ⚠️ 附近用户数据为模拟数据,需要真实位置服务
|
||||
|
||||
**预计工作量**: 3-4天(已完成)
|
||||
|
||||
#### 8. **搜索功能增强** ⭐ ⚠️ 部分完成
|
||||
**为什么重要**: 提升搜索体验
|
||||
|
||||
- [x] 实时搜索过滤(SearchActivity已实现)
|
||||
- [x] 空状态显示(SearchActivity已实现)
|
||||
- [ ] 实现搜索历史(本地存储)
|
||||
- [ ] 实现热门搜索(模拟数据)
|
||||
- [ ] 优化搜索性能
|
||||
- [ ] 添加搜索建议
|
||||
- [ ] 优化搜索性能(防抖、缓存)
|
||||
- [ ] 添加搜索建议(自动补全)
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
**已完成内容**:
|
||||
- ✅ SearchActivity已实现实时搜索过滤功能
|
||||
- ✅ SearchActivity已实现空状态显示(无搜索结果时显示提示)
|
||||
- ✅ 支持从MainActivity传递搜索关键词到SearchActivity
|
||||
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐ ⚠️ 待实现
|
||||
**待实现内容**:
|
||||
- ⚠️ 搜索历史功能(使用SharedPreferences存储最近搜索记录)
|
||||
- ⚠️ 热门搜索功能(显示热门搜索关键词)
|
||||
- ⚠️ 搜索性能优化(防抖处理,避免频繁过滤)
|
||||
- ⚠️ 搜索建议功能(输入时显示自动补全建议)
|
||||
|
||||
**预计工作量**: 2-3天(部分完成,剩余1-2天)
|
||||
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: 完善核心社交功能
|
||||
|
||||
- [x] 完善消息列表UI(会话列表)
|
||||
- [ ] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [ ] 优化消息列表性能(使用messageId)
|
||||
- [ ] 添加消息操作(复制、删除等)
|
||||
- [ ] 实现消息搜索(本地)
|
||||
- [x] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [x] 优化消息列表性能(使用messageId)
|
||||
- [x] 添加消息操作(复制、删除等)
|
||||
- [x] 实现消息搜索(本地)
|
||||
|
||||
**已完成内容**:
|
||||
- ✅ 会话列表展示(MessagesActivity)
|
||||
- ✅ 滑动删除/标记已读功能(MessagesActivity)
|
||||
- ✅ 消息发送和列表展示(ConversationActivity)
|
||||
- ✅ 未读消息徽章管理
|
||||
- ✅ ChatMessage已添加MessageStatus枚举(SENDING, SENT, READ)
|
||||
- ✅ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)- ConversationMessagesAdapter已实现
|
||||
- ✅ 消息长按菜单,支持复制和删除操作 - ConversationActivity已实现
|
||||
- ✅ DiffUtil已使用messageId作为唯一标识,提升列表更新性能 - ConversationMessagesAdapter已优化
|
||||
- ✅ MessagesActivity已实现搜索功能,支持按会话标题和消息内容搜索
|
||||
- ✅ 内存泄漏防护已实现(onDestroy中清理Handler延迟任务)- ConversationActivity已实现
|
||||
- ✅ 滚动行为已优化(使用smoothScrollToPosition)- ConversationActivity已实现
|
||||
|
||||
**待实现内容**:
|
||||
- ⚠️ 为ChatMessage添加MessageStatus枚举(发送中、已发送、已读)
|
||||
- ⚠️ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||
- ⚠️ 实现消息长按菜单,支持复制和删除操作
|
||||
- ⚠️ 优化DiffUtil,使用messageId作为唯一标识,提升列表更新性能(当前使用timestamp)
|
||||
- ⚠️ 在MessagesActivity中添加搜索功能,支持按会话标题和消息内容搜索
|
||||
- ⚠️ 优化消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
||||
- ⚠️ 添加内存泄漏防护(onDestroy中清理延迟任务)
|
||||
- ⚠️ 优化滚动行为(使用smoothScrollToPosition替代scrollToPosition)
|
||||
**待完善内容**:
|
||||
- ⚠️ 消息预览功能(处理图片/语音消息类型)- 当前仅支持文本消息
|
||||
- ⚠️ 图片/语音消息发送和接收功能
|
||||
- ⚠️ 实时消息接收(WebSocket集成,等待后端)
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
**预计工作量**: 3-4天(已完成)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -609,15 +653,30 @@
|
|||
|
||||
### 🔵 第四优先级(功能扩展)
|
||||
|
||||
#### 14. **作品功能(前端UI)** ⭐
|
||||
#### 14. **作品功能(前端UI)** ⭐ ⚠️ 部分完成
|
||||
**为什么重要**: 完善个人中心功能
|
||||
|
||||
- [ ] 实现作品列表UI
|
||||
- [x] 作品列表UI(ProfileActivity已实现基础UI)
|
||||
- [x] UserWorksAdapter已创建(使用演示数据)
|
||||
- [ ] 实现作品发布UI(数据仅本地存储)
|
||||
- [ ] 实现作品详情页面
|
||||
- [ ] 实现作品编辑UI
|
||||
- [ ] 作品数据模型(WorkItem)
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
**已完成内容**:
|
||||
- ✅ ProfileActivity中已实现作品标签页UI
|
||||
- ✅ UserWorksAdapter已创建,支持显示作品列表
|
||||
- ✅ 作品列表使用GridLayoutManager(3列布局)
|
||||
- ✅ 支持作品/收藏/赞过三个标签页切换
|
||||
|
||||
**待实现内容**:
|
||||
- ⚠️ 作品发布功能(当前显示"待接入"提示)
|
||||
- ⚠️ 作品详情页面(点击作品查看详情)
|
||||
- ⚠️ 作品编辑和删除功能
|
||||
- ⚠️ 作品数据模型(WorkItem),当前使用Integer列表(drawable资源ID)作为临时方案
|
||||
- ⚠️ 作品数据持久化(本地存储,等待后端API)
|
||||
|
||||
**预计工作量**: 3-4天(部分完成,剩余2-3天)
|
||||
|
||||
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
|
||||
**为什么重要**: 提升传播能力
|
||||
|
|
@ -698,17 +757,6 @@
|
|||
- [ ] 重构重复代码
|
||||
|
||||
**预计工作量**: 持续进行
|
||||
|
||||
#### 20. **测试** ⭐
|
||||
**为什么重要**: 保证代码质量
|
||||
|
||||
- [ ] 编写单元测试(工具类)
|
||||
- [ ] 编写 UI 测试(关键流程)
|
||||
- [ ] 性能测试
|
||||
- [ ] 兼容性测试
|
||||
|
||||
**预计工作量**: 持续进行
|
||||
|
||||
---
|
||||
|
||||
### 📋 暂不实现(需等待后端)
|
||||
|
|
@ -722,22 +770,23 @@
|
|||
- ❌ 支付功能(等待后端和支付SDK)
|
||||
- ❌ 推流功能(如需要,等待推流SDK集成)
|
||||
|
||||
**注意**: 数据持久化(Room数据库)相关功能暂不实现,当前使用SharedPreferences存储简单配置数据。
|
||||
|
||||
---
|
||||
|
||||
## 📅 开发时间估算
|
||||
|
||||
### 第一阶段(核心修复)- 约 8-13 天
|
||||
### 第一阶段(核心修复)- 约 5-8 天
|
||||
- 空状态和错误处理
|
||||
- 生命周期和内存管理
|
||||
- 统一加载状态
|
||||
- 数据持久化
|
||||
|
||||
### 第二阶段(架构优化)- 约 12-18 天
|
||||
### 第二阶段(架构优化)- 约 8-12 天
|
||||
- 前端架构优化
|
||||
- 分类筛选功能
|
||||
- 顶部标签页功能
|
||||
- 搜索功能增强
|
||||
- 消息功能完善
|
||||
- ~~分类筛选功能~~ ✅ 已完成
|
||||
- ~~顶部标签页功能~~ ✅ 已完成
|
||||
- 搜索功能增强(部分完成,剩余搜索历史和热门搜索)
|
||||
- ~~消息功能完善~~ ✅ 已完成
|
||||
|
||||
### 第三阶段(体验增强)- 约 9-13 天
|
||||
- 引导页面
|
||||
|
|
@ -745,18 +794,20 @@
|
|||
- 深色模式
|
||||
- 多屏幕适配
|
||||
|
||||
### 第四阶段(功能扩展)- 约 9-13 天
|
||||
### 第四阶段(功能扩展)- 约 3-6 天
|
||||
- 作品功能
|
||||
- 分享功能
|
||||
- 通知功能
|
||||
- 设置页面完善
|
||||
- ~~分享功能~~ ✅ 已完成
|
||||
- ~~通知功能~~ ✅ 已完成
|
||||
- ~~设置页面完善~~ ✅ 已完成
|
||||
|
||||
### 第五阶段(优化提升)- 持续进行
|
||||
- 性能优化
|
||||
- 代码质量
|
||||
- 测试
|
||||
|
||||
**总计**: 约 38-57 个工作日(约 2-3 个月,按单人开发估算)
|
||||
**总计**: 约 25-39 个工作日(约 1.5-2 个月,按单人开发估算)
|
||||
|
||||
**注意**: 部分功能已完成(分类筛选、顶部标签页、分享、通知、设置、消息功能完善),实际开发时间会相应减少。当前已完成约 60-70% 的前端功能开发。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -765,9 +816,9 @@
|
|||
如果想快速看到效果,建议按以下顺序:
|
||||
|
||||
1. **第1周**: 空状态 + 错误处理 + 内存泄漏修复
|
||||
2. **第2周**: 统一加载状态 + 数据持久化(Room)
|
||||
3. **第3周**: 架构优化(MVVM)+ 分类筛选
|
||||
4. **第4周**: 顶部标签页 + 搜索增强
|
||||
2. **第2周**: 统一加载状态 + 分类筛选
|
||||
3. **第3周**: 架构优化(MVVM)+ 顶部标签页
|
||||
4. **第4周**: 搜索增强 + 消息功能完善
|
||||
|
||||
这样可以在一个月内完成核心功能的前端完善。
|
||||
|
||||
|
|
@ -776,18 +827,20 @@
|
|||
## 📝 总结(前端开发视角)
|
||||
|
||||
### 当前状态
|
||||
- **整体完成度**: 约 60-70%(前端UI层面)
|
||||
- **核心UI功能**: 基本完成,但缺少完善
|
||||
- **用户体验**: 良好,但需要优化细节
|
||||
- **代码质量**: 中等,需要重构和优化
|
||||
- **数据层**: 仅使用 SharedPreferences,需要引入 Room
|
||||
- **整体完成度**: 约 80-85%(前端UI层面)
|
||||
- **核心UI功能**: 基本完成,大部分功能已实现前端逻辑
|
||||
- **用户体验**: 良好,已实现空状态、错误处理、加载状态等
|
||||
- **代码质量**: 中等,已实现部分工具类和统一管理
|
||||
- **数据层**: 使用 SharedPreferences 存储配置,内存缓存存储临时数据
|
||||
- **已完成功能**: 主界面(含顶部标签页、分类筛选)、直播间详情、个人资料、消息(含状态显示、长按菜单、搜索)、搜索(含实时过滤、空状态)、分享、通知、设置等
|
||||
- **最近完成**: 消息功能完善(状态显示、长按菜单、性能优化、搜索功能)
|
||||
|
||||
### 前端可独立完成的工作
|
||||
1. ✅ **UI/UX 完善**: 空状态、错误处理、加载状态
|
||||
2. ✅ **架构优化**: MVVM、Repository 模式、代码重构
|
||||
3. ✅ **数据持久化**: Room 数据库、本地缓存
|
||||
4. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
|
||||
5. ✅ **体验增强**: 引导页、动画、深色模式
|
||||
3. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
|
||||
4. ✅ **体验增强**: 引导页、动画、深色模式
|
||||
5. ✅ **工具类**: 分享功能、通知功能、设置页面、缓存管理
|
||||
|
||||
### 需要等待后端的功能
|
||||
1. ❌ **API 集成**: 等待后端接口定义
|
||||
|
|
@ -798,9 +851,9 @@
|
|||
### 前端开发建议
|
||||
1. **立即开始**: 空状态处理、内存泄漏修复、统一加载状态
|
||||
2. **第一周**: 完成基础架构和用户体验修复
|
||||
3. **第二周**: 引入 Room 数据库,实现本地数据持久化
|
||||
4. **第三周**: 架构优化,引入 MVVM 模式
|
||||
5. **第四周**: 功能完善,实现分类筛选、搜索增强等
|
||||
3. **第二周**: 架构优化,引入 MVVM 模式
|
||||
4. **第三周**: 功能完善,实现分类筛选、搜索增强等
|
||||
5. **第四周**: 体验增强,实现引导页、动画优化等
|
||||
|
||||
### 开发原则
|
||||
- ✅ **优先前端可独立完成的工作**
|
||||
|
|
@ -808,6 +861,7 @@
|
|||
- ✅ **保持代码结构便于后续对接后端**
|
||||
- ✅ **注重用户体验和代码质量**
|
||||
- ⏸️ **等待后端接口的功能先做UI,数据用模拟**
|
||||
- ⏸️ **暂不实现数据持久化(Room数据库),使用SharedPreferences存储简单配置**
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
349
android-app/项目进度汇报.md
Normal file
349
android-app/项目进度汇报.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# Android 直播应用 - 项目进度汇报
|
||||
|
||||
> **汇报时间**: 2024年
|
||||
> **项目名称**: Android 直播应用
|
||||
> **开发阶段**: 前端功能完善阶段
|
||||
|
||||
---
|
||||
|
||||
## 📊 项目整体进度
|
||||
|
||||
### 完成度概览
|
||||
- **整体完成度**: **80-85%**(前端UI层面)
|
||||
- **核心功能完成度**: **85-90%**
|
||||
- **代码质量**: 中等,已实现统一工具类和错误处理机制
|
||||
|
||||
### 主要成果
|
||||
✅ **已完成核心功能模块**: 主界面、直播间、个人中心、消息、搜索、分享、通知、设置等
|
||||
✅ **已完成基础架构**: 空状态处理、错误处理、加载状态管理、内存泄漏防护
|
||||
✅ **已完成用户体验优化**: 统一UI组件、搜索功能、消息状态显示等
|
||||
|
||||
---
|
||||
|
||||
## ✅ 本周/近期完成的工作
|
||||
|
||||
### 1. 消息功能完善 ⭐⭐⭐
|
||||
**状态**: ✅ 已完成
|
||||
|
||||
**完成内容**:
|
||||
- ✅ 实现消息状态显示(发送中、已发送、已读)
|
||||
- 在消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||
- 支持状态自动更新(发送中 → 已发送 → 已读)
|
||||
- ✅ 实现消息长按菜单
|
||||
- 支持复制消息内容
|
||||
- 支持删除消息(带确认对话框)
|
||||
- ✅ 优化消息列表性能
|
||||
- DiffUtil使用messageId作为唯一标识,提升列表更新性能
|
||||
- 使用smoothScrollToPosition优化滚动体验
|
||||
- ✅ 实现会话搜索功能
|
||||
- 支持按会话标题搜索
|
||||
- 支持按消息内容搜索
|
||||
- 实时过滤,响应迅速
|
||||
- ✅ 内存泄漏防护
|
||||
- 在onDestroy中清理Handler延迟任务
|
||||
- 确保Activity销毁时资源正确释放
|
||||
|
||||
**技术亮点**:
|
||||
- 使用ListAdapter + DiffUtil实现高效的列表更新
|
||||
- 消息状态通过Handler模拟真实发送流程
|
||||
- 搜索功能支持实时过滤,用户体验流畅
|
||||
|
||||
---
|
||||
|
||||
### 2. 搜索功能增强 ⭐⭐
|
||||
**状态**: ⚠️ 部分完成(70%)
|
||||
|
||||
**完成内容**:
|
||||
- ✅ 实时搜索过滤功能
|
||||
- 输入关键词即时显示搜索结果
|
||||
- 支持按房间标题和主播名称搜索
|
||||
- ✅ 空状态显示
|
||||
- 无搜索结果时显示友好提示
|
||||
- 支持从MainActivity传递搜索关键词
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 搜索历史功能(预计1天)
|
||||
- ⚠️ 热门搜索功能(预计1天)
|
||||
- ⚠️ 搜索建议/自动补全(预计1天)
|
||||
|
||||
---
|
||||
|
||||
### 3. 作品功能基础搭建 ⭐
|
||||
**状态**: ⚠️ 部分完成(30%)
|
||||
|
||||
**完成内容**:
|
||||
- ✅ 作品列表UI已实现
|
||||
- ProfileActivity中作品标签页UI完整
|
||||
- UserWorksAdapter已创建
|
||||
- 支持3列网格布局展示
|
||||
|
||||
**待完善**:
|
||||
- ⚠️ 作品发布功能(预计2天)
|
||||
- ⚠️ 作品详情页面(预计1天)
|
||||
- ⚠️ 作品数据模型完善(预计1天)
|
||||
|
||||
---
|
||||
|
||||
## 📈 已完成的核心功能模块
|
||||
|
||||
### 1. 主界面 (MainActivity) - 95%
|
||||
- ✅ 瀑布流布局展示直播间
|
||||
- ✅ 下拉刷新和自动轮询(15秒间隔)
|
||||
- ✅ 创建直播间功能
|
||||
- ✅ 搜索框和语音搜索
|
||||
- ✅ 分类标签筛选(CategoryFilterManager)
|
||||
- ✅ 顶部标签页(关注/发现/附近)
|
||||
- ✅ 底部导航栏和侧边栏菜单
|
||||
|
||||
### 2. 直播间详情 (RoomDetailActivity) - 90%
|
||||
- ✅ 直播流播放(HLS/FLV)
|
||||
- ✅ 全屏播放和横竖屏切换
|
||||
- ✅ 聊天功能(模拟)
|
||||
- ✅ 关注按钮和观看人数显示
|
||||
- ✅ 自动重连机制
|
||||
- ✅ 分享功能
|
||||
|
||||
### 3. 个人资料 (ProfileActivity) - 90%
|
||||
- ✅ 个人资料展示和编辑
|
||||
- ✅ 头像查看(大图预览)
|
||||
- ✅ 标签显示(地区、性别、年龄、星座)
|
||||
- ✅ 作品/收藏/赞过标签页
|
||||
- ✅ 关注/粉丝/获赞统计
|
||||
- ✅ 主页链接复制
|
||||
|
||||
### 4. 消息功能 (MessagesActivity + ConversationActivity) - 90%
|
||||
- ✅ 会话列表展示
|
||||
- ✅ 未读消息徽章管理
|
||||
- ✅ 滑动删除/标记已读
|
||||
- ✅ 消息发送和状态显示
|
||||
- ✅ 消息长按菜单(复制、删除)
|
||||
- ✅ 会话搜索功能
|
||||
- ✅ 内存泄漏防护
|
||||
|
||||
### 5. 搜索功能 (SearchActivity) - 70%
|
||||
- ✅ 搜索界面UI
|
||||
- ✅ 实时搜索过滤
|
||||
- ✅ 搜索结果展示
|
||||
- ✅ 空状态显示
|
||||
|
||||
### 6. 其他已完成功能
|
||||
- ✅ 分享功能(ShareUtils工具类)
|
||||
- ✅ 通知功能(NotificationsActivity + NotificationSettingsActivity)
|
||||
- ✅ 设置页面(SettingsPageActivity)
|
||||
- ✅ 登录/注册页面
|
||||
- ✅ 用户资料查看(UserProfileReadOnlyActivity)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术架构与工具类
|
||||
|
||||
### 已实现的工具类
|
||||
1. **ErrorHandler** - 统一错误处理和提示
|
||||
2. **EmptyStateView** - 统一空状态组件
|
||||
3. **LoadingStateManager** - 统一加载状态管理
|
||||
4. **NetworkRequestManager** - 生命周期感知的网络请求管理
|
||||
5. **CategoryFilterManager** - 分类筛选管理
|
||||
6. **LocationDataManager** - 位置数据管理
|
||||
7. **ShareUtils** - 分享功能工具类
|
||||
8. **LocalNotificationManager** - 本地通知管理
|
||||
9. **CacheManager** - 缓存管理
|
||||
10. **UnreadMessageManager** - 未读消息管理
|
||||
|
||||
### 数据存储
|
||||
- ✅ SharedPreferences - 用于个人资料和配置存储
|
||||
- ✅ 内存缓存 - 用于临时数据缓存
|
||||
- ✅ 网络层 - Retrofit + OkHttp 已配置完成
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 待完善功能(优先级排序)
|
||||
|
||||
### 🔴 高优先级(1-2周内完成)
|
||||
|
||||
#### 1. 搜索功能完善
|
||||
- [ ] 搜索历史(本地存储)
|
||||
- [ ] 热门搜索(模拟数据)
|
||||
- [ ] 搜索建议(自动补全)
|
||||
- **预计工作量**: 2-3天
|
||||
|
||||
#### 2. 作品功能完善
|
||||
- [ ] 作品发布UI
|
||||
- [ ] 作品详情页面
|
||||
- [ ] 作品数据模型
|
||||
- **预计工作量**: 3-4天
|
||||
|
||||
#### 3. 前端架构优化
|
||||
- [ ] 引入 MVVM 架构(ViewModel + LiveData)
|
||||
- [ ] 实现 Repository 模式
|
||||
- [ ] 提取公共基类 Activity
|
||||
- **预计工作量**: 5-7天
|
||||
|
||||
---
|
||||
|
||||
### 🟡 中优先级(2-4周内完成)
|
||||
|
||||
#### 4. 用户体验增强
|
||||
- [ ] 引导页面和帮助
|
||||
- [ ] 过渡动画和交互优化
|
||||
- [ ] 深色模式支持
|
||||
- [ ] 多屏幕适配
|
||||
- **预计工作量**: 9-13天
|
||||
|
||||
---
|
||||
|
||||
### 🟢 低优先级(持续优化)
|
||||
|
||||
#### 5. 性能优化
|
||||
- [ ] 图片加载优化(Glide配置)
|
||||
- [ ] 列表滚动性能优化
|
||||
- [ ] 请求去重机制
|
||||
- [ ] 内存优化
|
||||
- **预计工作量**: 3-4天
|
||||
|
||||
#### 6. 代码质量提升
|
||||
- [ ] 提取硬编码字符串到资源文件
|
||||
- [ ] 添加代码注释
|
||||
- [ ] 统一代码风格
|
||||
- [ ] 重构重复代码
|
||||
- **预计工作量**: 持续进行
|
||||
|
||||
---
|
||||
|
||||
## 📋 等待后端支持的功能
|
||||
|
||||
以下功能需要后端API支持,建议后端开发完成后再实现:
|
||||
|
||||
- ❌ 后端API完整集成(等待后端接口)
|
||||
- ❌ 实时通信(WebSocket,等待后端)
|
||||
- ❌ 真实数据同步(等待后端)
|
||||
- ❌ 用户登录/注册(等待后端)
|
||||
- ❌ 支付功能(等待后端和支付SDK)
|
||||
- ❌ 推流功能(如需要,等待推流SDK集成)
|
||||
|
||||
**当前策略**: 使用演示数据模拟后端接口,保持代码结构便于后续对接。
|
||||
|
||||
---
|
||||
|
||||
## 📅 下一步工作计划
|
||||
|
||||
### 本周计划(优先级排序)
|
||||
|
||||
1. **搜索功能完善**(2-3天)
|
||||
- 实现搜索历史(SharedPreferences存储)
|
||||
- 实现热门搜索(模拟数据)
|
||||
- 添加搜索建议功能
|
||||
|
||||
2. **作品功能完善**(3-4天)
|
||||
- 实现作品发布UI
|
||||
- 实现作品详情页面
|
||||
- 完善作品数据模型
|
||||
|
||||
3. **代码优化**(持续)
|
||||
- 修复已知问题
|
||||
- 优化代码结构
|
||||
- 添加必要注释
|
||||
|
||||
### 下周计划
|
||||
|
||||
1. **前端架构优化**(5-7天)
|
||||
- 引入 MVVM 架构
|
||||
- 实现 Repository 模式
|
||||
- 提取公共基类
|
||||
|
||||
2. **用户体验增强**(开始)
|
||||
- 引导页面
|
||||
- 过渡动画优化
|
||||
|
||||
---
|
||||
|
||||
## 📊 统计数据
|
||||
|
||||
### 代码统计
|
||||
- **Activity数量**: 30+ 个
|
||||
- **布局文件**: 50+ 个
|
||||
- **工具类**: 10+ 个
|
||||
- **适配器**: 15+ 个
|
||||
|
||||
### 功能统计
|
||||
- **已完成功能模块**: 8+ 个核心模块
|
||||
- **已完成工具类**: 10+ 个
|
||||
- **已完成页面**: 25+ 个页面
|
||||
|
||||
### 完成度统计
|
||||
- **整体完成度**: 80-85%
|
||||
- **核心功能完成度**: 85-90%
|
||||
- **UI完成度**: 90%+
|
||||
- **功能完成度**: 75-80%
|
||||
|
||||
---
|
||||
|
||||
## 🎯 项目亮点
|
||||
|
||||
### 1. 完善的用户体验
|
||||
- ✅ 统一的空状态处理
|
||||
- ✅ 统一的错误提示机制
|
||||
- ✅ 统一的加载状态管理
|
||||
- ✅ 友好的用户交互反馈
|
||||
|
||||
### 2. 良好的代码架构
|
||||
- ✅ 工具类统一管理
|
||||
- ✅ 生命周期感知的网络请求
|
||||
- ✅ 内存泄漏防护
|
||||
- ✅ 性能优化(DiffUtil、smoothScroll等)
|
||||
|
||||
### 3. 完整的功能实现
|
||||
- ✅ 消息功能完善(状态显示、长按菜单、搜索)
|
||||
- ✅ 搜索功能基础实现
|
||||
- ✅ 分享、通知、设置等辅助功能
|
||||
|
||||
### 4. 可扩展性
|
||||
- ✅ 代码结构便于对接后端API
|
||||
- ✅ 使用演示数据模拟,便于测试
|
||||
- ✅ 预留接口便于后续扩展
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 风险与挑战
|
||||
|
||||
### 1. 后端依赖
|
||||
- **风险**: 部分功能需要等待后端API支持
|
||||
- **应对**: 使用演示数据模拟,保持代码结构便于对接
|
||||
|
||||
### 2. 数据持久化
|
||||
- **风险**: 当前仅使用SharedPreferences和内存缓存
|
||||
- **应对**: 等待后端API后实现完整的数据同步机制
|
||||
|
||||
### 3. 实时通信
|
||||
- **风险**: WebSocket功能需要后端支持
|
||||
- **应对**: 当前使用模拟数据,等待后端WebSocket服务
|
||||
|
||||
---
|
||||
|
||||
## 💡 总结
|
||||
|
||||
### 当前状态
|
||||
项目前端开发进展顺利,**整体完成度达到80-85%**。核心功能模块基本完成,用户体验良好,代码质量持续提升。
|
||||
|
||||
### 主要成就
|
||||
1. ✅ **消息功能完善** - 实现了完整的消息状态显示、长按菜单、搜索等功能
|
||||
2. ✅ **搜索功能基础** - 实现了实时搜索过滤和空状态显示
|
||||
3. ✅ **工具类体系** - 建立了完善的工具类体系,提升代码复用性
|
||||
4. ✅ **用户体验** - 统一了空状态、错误处理、加载状态等用户体验
|
||||
|
||||
### 下一步重点
|
||||
1. 完善搜索功能(搜索历史、热门搜索)
|
||||
2. 完善作品功能(发布、详情页面)
|
||||
3. 前端架构优化(MVVM、Repository模式)
|
||||
|
||||
### 预计完成时间
|
||||
- **搜索功能完善**: 2-3天
|
||||
- **作品功能完善**: 3-4天
|
||||
- **前端架构优化**: 5-7天
|
||||
- **总计**: 约10-14个工作日(2-3周)
|
||||
|
||||
---
|
||||
|
||||
**汇报人**: [您的姓名]
|
||||
**日期**: 2024年
|
||||
**版本**: v1.0
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.example.livestreaming.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreateRoomRequest {
|
||||
|
||||
@NotBlank(message = "标题不能为空")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "主播名称不能为空")
|
||||
private String streamerName;
|
||||
|
||||
@NotBlank(message = "直播类型不能为空")
|
||||
private String type;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.example.livestreaming.dto;
|
||||
|
||||
import com.example.livestreaming.entity.Room;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RoomResponse {
|
||||
|
||||
private String id;
|
||||
private String title;
|
||||
private String streamerName;
|
||||
private String type;
|
||||
private String streamKey;
|
||||
private boolean isLive;
|
||||
private int viewerCount;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private StreamUrls streamUrls;
|
||||
|
||||
@Data
|
||||
public static class StreamUrls {
|
||||
private String rtmp;
|
||||
private String flv;
|
||||
private String hls;
|
||||
}
|
||||
|
||||
public static RoomResponse fromEntity(Room room, String rtmpHost, int rtmpPort, String httpHost, int httpPort) {
|
||||
RoomResponse response = new RoomResponse();
|
||||
response.setId(room.getId());
|
||||
response.setTitle(room.getTitle());
|
||||
response.setStreamerName(room.getStreamerName());
|
||||
response.setType(room.getType());
|
||||
response.setStreamKey(room.getStreamKey());
|
||||
response.setLive(room.isLive());
|
||||
response.setViewerCount(room.getViewerCount());
|
||||
response.setCreatedAt(room.getCreatedAt() != null ? room.getCreatedAt().toString() : null);
|
||||
response.setStartedAt(room.getStartedAt() != null ? room.getStartedAt().toString() : null);
|
||||
|
||||
// 构建流地址
|
||||
StreamUrls urls = new StreamUrls();
|
||||
String streamKey = room.getStreamKey();
|
||||
urls.setRtmp(String.format("rtmp://%s:%d/live/%s", rtmpHost, rtmpPort, streamKey));
|
||||
urls.setFlv(String.format("http://%s:%d/live/%s.flv", httpHost, httpPort, streamKey));
|
||||
urls.setHls(String.format("http://%s:%d/live/%s/index.m3u8", httpHost, httpPort, streamKey));
|
||||
response.setStreamUrls(urls);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user