修复了一些bug和添加了TODO
This commit is contained in:
parent
484c17a4d3
commit
171054efbb
|
|
@ -138,6 +138,16 @@
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
android:screenOrientation="portrait" />
|
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
|
<activity
|
||||||
android:name="com.example.livestreaming.MainActivity"
|
android:name="com.example.livestreaming.MainActivity"
|
||||||
android:exported="true"
|
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) {
|
public void filterRoomsAsync(List<Room> allRooms, String category, FilterCallback callback) {
|
||||||
if (executorService == null || executorService.isShutdown()) {
|
if (executorService == null || executorService.isShutdown()) {
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,11 @@ public class ConversationActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessage() {
|
private void sendMessage() {
|
||||||
|
// 检查登录状态,发送私信需要登录
|
||||||
|
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 接入后端接口 - 发送私信消息
|
// TODO: 接入后端接口 - 发送私信消息
|
||||||
// 接口路径: POST /api/conversations/{conversationId}/messages
|
// 接口路径: POST /api/conversations/{conversationId}/messages
|
||||||
// 请求参数:
|
// 请求参数:
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ public class EditProfileActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// 检查登录状态,编辑资料需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
|
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
|
@ -136,6 +143,24 @@ public class EditProfileActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.saveButton.setOnClickListener(v -> {
|
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 name = binding.inputName.getText() != null ? binding.inputName.getText().toString().trim() : "";
|
||||||
String bio = binding.inputBio.getText() != null ? binding.inputBio.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() : "";
|
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) {
|
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;
|
if (sourceUri == null) return null;
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
OutputStream out = null;
|
OutputStream out = null;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ public class FriendsAdapter extends ListAdapter<FriendItem, FriendsAdapter.VH> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(FriendItem item) {
|
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.name.setText(item != null && item.getName() != null ? item.getName() : "");
|
||||||
binding.subtitle.setText(item != null && item.getSubtitle() != null ? item.getSubtitle() : "");
|
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) {
|
public static void sendNotification(Context context, String title, String content, NotificationItem.Type type) {
|
||||||
if (context == null || title == null || content == null) return;
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
|
@ -173,49 +187,81 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private void handleDrawerAction(DrawerCardItem item) {
|
private void handleDrawerAction(DrawerCardItem item) {
|
||||||
int action = item.getAction();
|
int action = item.getAction();
|
||||||
if (action == DrawerCardItem.ACTION_PROFILE) {
|
if (action == DrawerCardItem.ACTION_PROFILE) {
|
||||||
|
// 检查登录状态,个人主页需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ProfileActivity.start(this);
|
ProfileActivity.start(this);
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_MESSAGES) {
|
if (action == DrawerCardItem.ACTION_MESSAGES) {
|
||||||
|
// MessagesActivity内部已检查登录,这里直接跳转
|
||||||
MessagesActivity.start(this);
|
MessagesActivity.start(this);
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_MY_FRIENDS) {
|
if (action == DrawerCardItem.ACTION_MY_FRIENDS) {
|
||||||
|
// 检查登录状态,我的好友需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看好友列表需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
startActivity(new Intent(this, MyFriendsActivity.class));
|
startActivity(new Intent(this, MyFriendsActivity.class));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_FISH_POND) {
|
if (action == DrawerCardItem.ACTION_FISH_POND) {
|
||||||
|
// 检查登录状态,缘池功能需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "缘池功能需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
startActivity(new Intent(this, FishPondActivity.class));
|
startActivity(new Intent(this, FishPondActivity.class));
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_FOLLOWING) {
|
if (action == DrawerCardItem.ACTION_FOLLOWING) {
|
||||||
|
// 检查登录状态,我的关注需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看关注列表需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
FollowingListActivity.start(this);
|
FollowingListActivity.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_FANS) {
|
if (action == DrawerCardItem.ACTION_FANS) {
|
||||||
|
// 检查登录状态,我的粉丝需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看粉丝列表需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
FansListActivity.start(this);
|
FansListActivity.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_LIKES) {
|
if (action == DrawerCardItem.ACTION_LIKES) {
|
||||||
|
// 检查登录状态,获赞列表需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看获赞列表需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
LikesListActivity.start(this);
|
LikesListActivity.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_HISTORY) {
|
if (action == DrawerCardItem.ACTION_HISTORY) {
|
||||||
|
// 检查登录状态,观看历史需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看观看历史需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
WatchHistoryActivity.start(this);
|
WatchHistoryActivity.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_SEARCH) {
|
if (action == DrawerCardItem.ACTION_SEARCH) {
|
||||||
|
// 搜索功能不需要登录,游客也可以搜索
|
||||||
SearchActivity.start(this);
|
SearchActivity.start(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_SETTINGS) {
|
if (action == DrawerCardItem.ACTION_SETTINGS) {
|
||||||
|
// SettingsPageActivity内部已检查登录,这里直接跳转
|
||||||
SettingsPageActivity.start(this, "");
|
SettingsPageActivity.start(this, "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action == DrawerCardItem.ACTION_HELP) {
|
if (action == DrawerCardItem.ACTION_HELP) {
|
||||||
|
// 帮助页面不需要登录
|
||||||
SettingsPageActivity.start(this, SettingsPageActivity.PAGE_HELP);
|
SettingsPageActivity.start(this, SettingsPageActivity.PAGE_HELP);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +320,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.avatarButton.setOnClickListener(v -> {
|
binding.avatarButton.setOnClickListener(v -> {
|
||||||
|
// 检查登录状态,个人主页需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
ProfileActivity.start(this);
|
ProfileActivity.start(this);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
@ -408,6 +458,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (id == R.id.nav_profile) {
|
if (id == R.id.nav_profile) {
|
||||||
|
// 检查登录状态,个人主页需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看个人主页需要登录")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
ProfileActivity.start(this);
|
ProfileActivity.start(this);
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -654,6 +708,32 @@ public class MainActivity extends AppCompatActivity {
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
|
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.setOnShowListener(d -> {
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
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 title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
|
||||||
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.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() {
|
private void initializeTopTabData() {
|
||||||
// 初始化关注页面数据(已关注主播的直播)
|
// 初始化关注页面数据(已关注主播的直播)
|
||||||
|
|
@ -1498,16 +1590,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
* 显示附近页面
|
* 显示附近页面
|
||||||
*/
|
*/
|
||||||
private void showNearbyTab() {
|
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) {
|
if (binding.categoryTabs != null) {
|
||||||
binding.categoryTabs.setVisibility(View.GONE);
|
binding.categoryTabs.setVisibility(View.GONE);
|
||||||
|
|
@ -1526,10 +1609,37 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (binding.roomsRecyclerView != null) {
|
if (binding.roomsRecyclerView != null) {
|
||||||
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
|
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
|
||||||
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
|
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
|
||||||
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
|
|
||||||
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
|
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 (nearbyUsers.isEmpty()) {
|
||||||
if (binding.emptyStateView != null) {
|
if (binding.emptyStateView != null) {
|
||||||
|
|
@ -1708,11 +1818,18 @@ public class MainActivity extends AppCompatActivity {
|
||||||
* 请求位置权限
|
* 请求位置权限
|
||||||
*/
|
*/
|
||||||
private void requestLocationPermission() {
|
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)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("需要位置权限")
|
.setTitle("需要位置权限")
|
||||||
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
|
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。")
|
||||||
.setPositiveButton("确定", (dialog, which) -> {
|
.setPositiveButton("授权", (dialog, which) -> {
|
||||||
|
// 直接请求权限
|
||||||
ActivityCompat.requestPermissions(this,
|
ActivityCompat.requestPermissions(this,
|
||||||
new String[]{
|
new String[]{
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
|
@ -1720,19 +1837,13 @@ public class MainActivity extends AppCompatActivity {
|
||||||
},
|
},
|
||||||
REQUEST_LOCATION_PERMISSION);
|
REQUEST_LOCATION_PERMISSION);
|
||||||
})
|
})
|
||||||
.setNegativeButton("取消", (dialog, which) -> {
|
.setNegativeButton("取消", null)
|
||||||
// 用户拒绝权限,显示提示
|
.setCancelable(true)
|
||||||
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
|
|
||||||
// 切换回发现页面
|
|
||||||
if (binding.topTabs != null) {
|
|
||||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
|
||||||
if (discoverTab != null) {
|
|
||||||
discoverTab.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
|
// 第一次请求权限,或者用户选择了"不再询问"
|
||||||
|
// 直接请求权限,如果用户选择了"不再询问",系统会静默失败
|
||||||
|
// 我们会在权限回调中处理这种情况
|
||||||
ActivityCompat.requestPermissions(this,
|
ActivityCompat.requestPermissions(this,
|
||||||
new String[]{
|
new String[]{
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,15 @@ public class MyFriendsActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilter(String query) {
|
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()) {
|
if (query == null || query.trim().isEmpty()) {
|
||||||
adapter.submitList(new ArrayList<>(all));
|
adapter.submitList(new ArrayList<>(all));
|
||||||
updateEmptyState(all);
|
updateEmptyState(all);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.ListAdapter;
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.example.livestreaming.databinding.ItemNearbyUserBinding;
|
import com.example.livestreaming.databinding.ItemNearbyUserBinding;
|
||||||
|
|
||||||
public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapter.VH> {
|
public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapter.VH> {
|
||||||
|
|
@ -48,29 +49,51 @@ public class NearbyUsersAdapter extends ListAdapter<NearbyUser, NearbyUsersAdapt
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(NearbyUser user) {
|
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() : "";
|
binding.userName.setText(user.getName() != null ? user.getName() : "");
|
||||||
if (!distanceText.isEmpty() && !distanceText.startsWith("距离")) {
|
|
||||||
binding.distanceText.setText("距离 " + distanceText);
|
// 格式化距离文本(移除"距离"前缀,因为布局中已经有位置图标)
|
||||||
} else {
|
String distanceText = user.getDistanceText() != null ? user.getDistanceText() : "";
|
||||||
binding.distanceText.setText(distanceText);
|
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 -> {
|
binding.addButton.setOnClickListener(v -> {
|
||||||
if (user == null) return;
|
if (onUserClickListener != null) {
|
||||||
if (onUserClickListener != null) onUserClickListener.onUserClick(user);
|
onUserClickListener.onUserClick(user);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 整个item点击也可以触发添加
|
// 整个item点击也可以触发添加
|
||||||
binding.getRoot().setOnClickListener(v -> {
|
binding.getRoot().setOnClickListener(v -> {
|
||||||
if (user == null) return;
|
if (onUserClickListener != null) {
|
||||||
if (onUserClickListener != null) onUserClickListener.onUserClick(user);
|
onUserClickListener.onUserClick(user);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,14 @@ public class NotificationSettingsActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshItems() {
|
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<>();
|
List<MoreItem> items = new ArrayList<>();
|
||||||
|
|
||||||
// 系统通知开关
|
// 系统通知开关
|
||||||
|
|
@ -123,6 +131,20 @@ public class NotificationSettingsActivity extends AppCompatActivity {
|
||||||
.setTitle("免打扰设置")
|
.setTitle("免打扰设置")
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.setPositiveButton("开启", (dialog, which) -> {
|
.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();
|
prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply();
|
||||||
refreshItems();
|
refreshItems();
|
||||||
Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ public class NotificationsActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// 检查登录状态,通知列表需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "查看通知需要登录")) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
|
binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,11 @@ public class NotificationsAdapter extends ListAdapter<NotificationItem, Notifica
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAvatar(NotificationItem item) {
|
private void loadAvatar(NotificationItem item) {
|
||||||
|
// TODO: 接入后端接口 - 从后端获取通知发送者的头像URL
|
||||||
|
// 接口路径: GET /api/user/profile/{senderId}
|
||||||
|
// 请求参数: senderId (路径参数,从NotificationItem中获取)
|
||||||
|
// 返回数据格式: ApiResponse<{avatarUrl: string}>
|
||||||
|
// 如果NotificationItem包含senderId,则调用此接口获取头像;否则使用默认图标
|
||||||
if (binding.avatar == null) return;
|
if (binding.avatar == null) return;
|
||||||
|
|
||||||
String avatarUrl = item.getAvatarUrl();
|
String avatarUrl = item.getAvatarUrl();
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,15 @@ public class PlayerActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.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);
|
String url = getIntent().getStringExtra(EXTRA_PLAY_URL);
|
||||||
if (url == null || url.trim().isEmpty()) return;
|
if (url == null || url.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
|
@ -115,6 +124,15 @@ public class PlayerActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
|
// TODO: 接入后端接口 - 记录播放结束
|
||||||
|
// 接口路径: POST /api/play/end
|
||||||
|
// 请求参数:
|
||||||
|
// - userId: 当前用户ID(从token中获取,可选)
|
||||||
|
// - roomId: 房间ID(从Intent中获取)
|
||||||
|
// - playDuration: 播放时长(秒)
|
||||||
|
// - timestamp: 结束播放时间戳
|
||||||
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||||
|
// 用于统计播放时长和用户观看行为
|
||||||
releaseExoPlayer();
|
releaseExoPlayer();
|
||||||
releaseIjkPlayer();
|
releaseIjkPlayer();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,13 @@ public class ProfileActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private void setupNavigationClicks() {
|
private void setupNavigationClicks() {
|
||||||
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
|
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.topActionMore.setOnClickListener(v -> TabPlaceholderActivity.start(this, "更多"));
|
||||||
|
|
||||||
binding.copyIdBtn.setOnClickListener(v -> {
|
binding.copyIdBtn.setOnClickListener(v -> {
|
||||||
|
|
@ -267,19 +273,53 @@ public class ProfileActivity extends AppCompatActivity {
|
||||||
// - userId: 用户ID(路径参数)
|
// - userId: 用户ID(路径参数)
|
||||||
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
|
// 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
|
||||||
// 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
|
// 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
|
||||||
binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
|
binding.following.setOnClickListener(v -> {
|
||||||
binding.followers.setOnClickListener(v -> FansListActivity.start(this));
|
// 检查登录状态,查看关注列表需要登录
|
||||||
binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
|
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.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.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
|
||||||
|
|
||||||
binding.editProfile.setOnClickListener(v -> {
|
binding.editProfile.setOnClickListener(v -> {
|
||||||
|
// 检查登录状态,编辑资料需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||||
editProfileLauncher.launch(intent);
|
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, "加好友"));
|
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.likedGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
|
||||||
binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
|
binding.favGoBrowseBtn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)));
|
||||||
binding.profileEditFromTab.setOnClickListener(v -> {
|
binding.profileEditFromTab.setOnClickListener(v -> {
|
||||||
|
// 检查登录状态,编辑资料需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "编辑资料需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||||
editProfileLauncher.launch(intent);
|
editProfileLauncher.launch(intent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showTab(int index) {
|
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-赞过
|
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
||||||
binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
|
binding.tabWorks.setVisibility(index == 0 ? View.VISIBLE : View.GONE);
|
||||||
binding.tabFavorites.setVisibility(index == 1 ? 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();
|
setupUI();
|
||||||
setupChat();
|
setupChat();
|
||||||
|
|
||||||
|
// TODO: 接入后端接口 - 记录观看历史
|
||||||
|
// 接口路径: POST /api/watch/history
|
||||||
|
// 请求参数:
|
||||||
|
// - userId: 当前用户ID(从token中获取)
|
||||||
|
// - roomId: 房间ID
|
||||||
|
// - watchTime: 观看时间(时间戳,可选)
|
||||||
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||||
|
// 进入房间时调用,用于记录用户观看历史
|
||||||
// 添加欢迎消息
|
// 添加欢迎消息
|
||||||
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
|
addChatMessage(new ChatMessage("欢迎来到直播间!", true));
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +147,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
// 返回数据格式: ApiResponse<{success: boolean, message: string}>
|
||||||
// 关注成功后,更新按钮状态为"已关注",并禁用按钮
|
// 关注成功后,更新按钮状态为"已关注",并禁用按钮
|
||||||
binding.followButton.setOnClickListener(v -> {
|
binding.followButton.setOnClickListener(v -> {
|
||||||
|
// 检查登录状态,关注主播需要登录
|
||||||
|
if (!AuthHelper.requireLogin(this, "关注主播需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
|
||||||
binding.followButton.setText("已关注");
|
binding.followButton.setText("已关注");
|
||||||
binding.followButton.setEnabled(false);
|
binding.followButton.setEnabled(false);
|
||||||
|
|
@ -171,6 +184,11 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessage() {
|
private void sendMessage() {
|
||||||
|
// 检查登录状态,发送弹幕需要登录
|
||||||
|
if (!AuthHelper.requireLoginWithToast(this, "发送弹幕需要登录")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 接入后端接口 - 发送直播间弹幕消息
|
// TODO: 接入后端接口 - 发送直播间弹幕消息
|
||||||
// 接口路径: POST /api/rooms/{roomId}/messages
|
// 接口路径: 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.roomTitle.setText(room != null && room.getTitle() != null ? room.getTitle() : "(Untitled)");
|
||||||
binding.streamerName.setText(room != null && room.getStreamerName() != null ? room.getStreamerName() : "");
|
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 {
|
try {
|
||||||
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
|
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
|
||||||
int h = Math.abs(seed.hashCode());
|
int h = Math.abs(seed.hashCode());
|
||||||
|
|
@ -114,14 +127,27 @@ public class RoomsAdapter extends ListAdapter<Room, RoomsAdapter.RoomVH> {
|
||||||
assetFile = a.coverAssetFiles.get(idx);
|
assetFile = a.coverAssetFiles.get(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 接入后端接口 - 从后端获取房间封面图片URL
|
||||||
|
// 接口路径: GET /api/rooms/{roomId}/cover
|
||||||
|
// 请求参数: roomId (路径参数)
|
||||||
|
// 返回数据格式: ApiResponse<{coverUrl: string}>
|
||||||
|
// 或者Room对象应包含coverUrl字段,直接从room.getCoverUrl()获取
|
||||||
|
// 优先使用Room对象中的coverUrl,如果没有则使用默认占位图
|
||||||
Object model;
|
Object model;
|
||||||
if (assetFile != null) {
|
if (assetFile != null) {
|
||||||
model = "file:///android_asset/img/" + assetFile;
|
model = "file:///android_asset/img/" + assetFile;
|
||||||
} else {
|
} else {
|
||||||
String seed = room != null && room.getId() != null ? room.getId() : String.valueOf(getBindingAdapterPosition());
|
// 优先从Room对象获取封面URL
|
||||||
int h = Math.abs(seed.hashCode());
|
String coverUrl = room != null ? room.getCoverUrl() : null;
|
||||||
int imageId = (h % 1000) + 1;
|
if (coverUrl != null && !coverUrl.trim().isEmpty()) {
|
||||||
model = "https://picsum.photos/id/" + imageId + "/600/450";
|
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)
|
Glide.with(binding.coverImage)
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,23 @@ public class SearchActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilter(String q) {
|
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() : "";
|
String query = q != null ? q.trim() : "";
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
adapter.submitList(new ArrayList<>(all));
|
adapter.submitList(new ArrayList<>(all));
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ public class SearchSuggestionsAdapter extends ListAdapter<Room, SearchSuggestion
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(Room room) {
|
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 title = room != null && room.getTitle() != null ? room.getTitle() : "";
|
||||||
String sub = room != null && room.getStreamerName() != null ? room.getStreamerName() : "";
|
String sub = room != null && room.getStreamerName() != null ? room.getStreamerName() : "";
|
||||||
binding.title.setText(title);
|
binding.title.setText(title);
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,22 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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());
|
binding = ActivitySettingsPageBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
binding.backButton.setOnClickListener(v -> finish());
|
binding.backButton.setOnClickListener(v -> finish());
|
||||||
|
|
||||||
page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
// page已在onCreate开始处获取
|
||||||
if (page == null) page = "";
|
if (page == null) page = "";
|
||||||
|
|
||||||
String title = resolveTitle(page);
|
String title = resolveTitle(page);
|
||||||
|
|
@ -478,6 +488,14 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showChangePasswordDialog() {
|
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);
|
EditText oldPassword = new EditText(this);
|
||||||
oldPassword.setHint("请输入旧密码");
|
oldPassword.setHint("请输入旧密码");
|
||||||
oldPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
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() {
|
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);
|
EditText phoneInput = new EditText(this);
|
||||||
phoneInput.setHint("请输入手机号");
|
phoneInput.setHint("请输入手机号");
|
||||||
phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
|
phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
|
||||||
|
|
@ -518,6 +550,19 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeviceManagementDialog() {
|
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)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("登录设备管理")
|
.setTitle("登录设备管理")
|
||||||
.setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
|
.setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
|
||||||
|
|
@ -526,6 +571,26 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showBlacklistDialog() {
|
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)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("黑名单管理")
|
.setTitle("黑名单管理")
|
||||||
.setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
|
.setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
|
||||||
|
|
@ -655,6 +720,18 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
"A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
|
"A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
|
||||||
"Q7: 如何举报不良内容?\n" +
|
"Q7: 如何举报不良内容?\n" +
|
||||||
"A: 在直播间或用户主页可以举报违规内容,我们会及时处理。\n\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)
|
new AlertDialog.Builder(this)
|
||||||
|
|
@ -665,6 +742,15 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFeedbackDialog() {
|
private void showFeedbackDialog() {
|
||||||
|
// TODO: 接入后端接口 - 提交意见反馈
|
||||||
|
// 接口路径: POST /api/feedback
|
||||||
|
// 请求参数:
|
||||||
|
// - userId: 用户ID(从token中获取,可选)
|
||||||
|
// - content: 反馈内容(必填)
|
||||||
|
// - contact: 联系方式(可选,邮箱或手机号)
|
||||||
|
// - images (可选): 反馈图片URL列表
|
||||||
|
// 返回数据格式: ApiResponse<{success: boolean, feedbackId: string}>
|
||||||
|
// 提交成功后,提示用户并关闭对话框
|
||||||
EditText feedbackInput = new EditText(this);
|
EditText feedbackInput = new EditText(this);
|
||||||
feedbackInput.setHint("请输入您的意见或建议");
|
feedbackInput.setHint("请输入您的意见或建议");
|
||||||
feedbackInput.setMinLines(5);
|
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) {
|
public static String generateProfileShareLink(String userId) {
|
||||||
return "https://livestreaming.com/profile/" + 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) {
|
public static String generateRoomShareLink(String roomId) {
|
||||||
return "https://livestreaming.com/room/" + 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) {
|
public static void shareLink(Context context, String link, String title, String text) {
|
||||||
if (context == null || link == null) return;
|
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.discoverShortcutLive.setOnClickListener(v -> Toast.makeText(this, "进入直播模块", Toast.LENGTH_SHORT).show());
|
||||||
binding.discoverShortcutNearby.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
|
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());
|
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());
|
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) {
|
private void applyDiscoverFilter(String q) {
|
||||||
|
// TODO: 接入后端接口 - 发现页搜索建议
|
||||||
|
// 接口路径: GET /api/discover/search/suggestions
|
||||||
|
// 请求参数:
|
||||||
|
// - keyword: 搜索关键词(必填)
|
||||||
|
// - limit (可选): 返回数量限制,默认10
|
||||||
|
// 返回数据格式: ApiResponse<List<Room>>
|
||||||
|
// 用于在发现页搜索框输入时显示实时搜索建议
|
||||||
String query = q != null ? q.trim() : "";
|
String query = q != null ? q.trim() : "";
|
||||||
if (TextUtils.isEmpty(query)) {
|
if (TextUtils.isEmpty(query)) {
|
||||||
binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
|
binding.discoverSuggestionsRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
@ -227,6 +248,13 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
|
|
||||||
ensureFollowRoomsAdapter();
|
ensureFollowRoomsAdapter();
|
||||||
|
|
||||||
|
// TODO: 接入后端接口 - 获取关注主播的直播间列表(TabPlaceholderActivity中的关注页面)
|
||||||
|
// 接口路径: GET /api/following/rooms
|
||||||
|
// 请求参数:
|
||||||
|
// - userId: 当前用户ID(从token中获取)
|
||||||
|
// - page (可选): 页码
|
||||||
|
// - pageSize (可选): 每页数量
|
||||||
|
// 返回数据格式: ApiResponse<List<Room>>
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
followAllRooms.clear();
|
followAllRooms.clear();
|
||||||
followAllRooms.addAll(buildFollowDemoRooms(16));
|
followAllRooms.addAll(buildFollowDemoRooms(16));
|
||||||
|
|
@ -327,6 +355,14 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.nearbyRecyclerView.setLayoutManager(layoutManager);
|
binding.nearbyRecyclerView.setLayoutManager(layoutManager);
|
||||||
binding.nearbyRecyclerView.setAdapter(adapter);
|
binding.nearbyRecyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
// TODO: 接入后端接口 - 获取附近用户列表(TabPlaceholderActivity中的附近页面)
|
||||||
|
// 接口路径: GET /api/users/nearby
|
||||||
|
// 请求参数:
|
||||||
|
// - latitude: 当前用户纬度(必填)
|
||||||
|
// - longitude: 当前用户经度(必填)
|
||||||
|
// - radius (可选): 搜索半径(单位:米,默认5000)
|
||||||
|
// - page (可选): 页码
|
||||||
|
// 返回数据格式: ApiResponse<List<NearbyUser>>
|
||||||
adapter.submitList(buildNearbyDemoUsers(18));
|
adapter.submitList(buildNearbyDemoUsers(18));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,6 +443,12 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.parkBadgeRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
binding.parkBadgeRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
|
||||||
binding.parkBadgeRecyclerView.setAdapter(badgesAdapter);
|
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());
|
badgesAdapter.submitList(buildDemoBadges());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,14 +590,21 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLogoutConfirm() {
|
private void showLogoutConfirm() {
|
||||||
|
// TODO: 接入后端接口 - 退出登录
|
||||||
|
// 接口路径: POST /api/logout 或 GET /api/logout
|
||||||
|
// 请求参数: 无(从token中获取userId)
|
||||||
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
||||||
|
// 退出成功后,清除本地token和用户信息,跳转到登录页面
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("退出登录")
|
.setTitle("退出登录")
|
||||||
.setMessage("确定要退出登录吗?")
|
.setMessage("确定要退出登录吗?")
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
.setPositiveButton("退出", (d, w) -> {
|
.setPositiveButton("退出", (d, w) -> {
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
// 清除token
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
com.example.livestreaming.net.AuthStore.setToken(getApplicationContext(), null);
|
||||||
startActivity(intent);
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
LoginActivity.start(TabPlaceholderActivity.this);
|
||||||
finish();
|
finish();
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
|
|
@ -572,9 +621,34 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
setFollowContainerVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(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.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
|
||||||
binding.nearbyEntryUsers.setOnClickListener(v -> TabPlaceholderActivity.start(this, "附近"));
|
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());
|
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());
|
binding.nearbyEntryPlaces.setOnClickListener(v -> Toast.makeText(this, "热门地点(待接入)", Toast.LENGTH_SHORT).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,12 +671,36 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
|
|
||||||
addFriendAdapter = new NearbyUsersAdapter(user -> {
|
addFriendAdapter = new NearbyUsersAdapter(user -> {
|
||||||
if (user == null) return;
|
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();
|
Toast.makeText(this, "已发送好友请求:" + user.getName(), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.addFriendRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
binding.addFriendRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
binding.addFriendRecyclerView.setAdapter(addFriendAdapter);
|
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.clear();
|
||||||
addFriendAllUsers.addAll(buildNearbyDemoUsers(18));
|
addFriendAllUsers.addAll(buildNearbyDemoUsers(18));
|
||||||
addFriendAdapter.submitList(new ArrayList<>(addFriendAllUsers));
|
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) {
|
public static int getUnreadCount(Context context) {
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,27 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showTab(int index) {
|
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;
|
if (binding == null) return;
|
||||||
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
// 标签页顺序:0-作品, 1-收藏, 2-赞过
|
||||||
binding.worksRecycler.setVisibility(index == 0 ? android.view.View.VISIBLE : android.view.View.GONE);
|
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<>();
|
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) {
|
public void submitList(List<Integer> list) {
|
||||||
items.clear();
|
items.clear();
|
||||||
if (list != null) items.addAll(list);
|
if (list != null) items.addAll(list);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapt
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(Room room) {
|
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;
|
if (room == null) return;
|
||||||
|
|
||||||
// 设置标题
|
// 设置标题
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,13 @@ public class WishTreeActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
// TODO: 接入后端接口 - 获取愿望树相关数据
|
||||||
|
// 接口路径: GET /api/wish_tree/info
|
||||||
|
// 请求参数:
|
||||||
|
// - userId: 当前用户ID(从token中获取,可选)
|
||||||
|
// 返回数据格式: ApiResponse<WishTreeInfo>
|
||||||
|
// WishTreeInfo对象应包含: totalWishes, todayWishes, userWishCount, nextResetTime等字段
|
||||||
|
// 用于显示愿望树统计信息和倒计时
|
||||||
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
|
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ public interface ApiService {
|
||||||
@POST("api/front/login")
|
@POST("api/front/login")
|
||||||
Call<ApiResponse<LoginResponse>> login(@Body LoginRequest body);
|
Call<ApiResponse<LoginResponse>> login(@Body LoginRequest body);
|
||||||
|
|
||||||
|
@POST("api/front/register")
|
||||||
|
Call<ApiResponse<LoginResponse>> register(@Body RegisterRequest body);
|
||||||
|
|
||||||
@GET("api/rooms")
|
@GET("api/rooms")
|
||||||
Call<ApiResponse<List<Room>>> getRooms();
|
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")
|
@SerializedName("viewerCount")
|
||||||
private int viewerCount;
|
private int viewerCount;
|
||||||
|
|
||||||
|
@SerializedName("coverUrl")
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
@SerializedName("streamUrls")
|
@SerializedName("streamUrls")
|
||||||
private StreamUrls streamUrls;
|
private StreamUrls streamUrls;
|
||||||
|
|
||||||
|
|
@ -72,6 +75,10 @@ public class Room {
|
||||||
this.viewerCount = viewerCount;
|
this.viewerCount = viewerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCoverUrl(String coverUrl) {
|
||||||
|
this.coverUrl = coverUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +111,10 @@ public class Room {
|
||||||
return viewerCount;
|
return viewerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCoverUrl() {
|
||||||
|
return coverUrl;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
|
@ -116,11 +127,12 @@ public class Room {
|
||||||
&& Objects.equals(streamerName, room.streamerName)
|
&& Objects.equals(streamerName, room.streamerName)
|
||||||
&& Objects.equals(type, room.type)
|
&& Objects.equals(type, room.type)
|
||||||
&& Objects.equals(streamKey, room.streamKey)
|
&& Objects.equals(streamKey, room.streamKey)
|
||||||
|
&& Objects.equals(coverUrl, room.coverUrl)
|
||||||
&& Objects.equals(streamUrls, room.streamUrls);
|
&& Objects.equals(streamUrls, room.streamUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="72dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="@android:color/white"
|
android:layout_marginStart="16dp"
|
||||||
android:paddingStart="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:paddingEnd="16dp">
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@drawable/bg_nearby_user_card_shadow"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
<ImageView
|
<!-- 头像容器,带直播状态指示 -->
|
||||||
android:id="@+id/avatarImage"
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="44dp"
|
android:id="@+id/avatarContainer"
|
||||||
android:layout_height="44dp"
|
android:layout_width="56dp"
|
||||||
android:background="@drawable/bg_avatar_circle"
|
android:layout_height="56dp"
|
||||||
android:padding="6dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:src="@drawable/ic_account_circle_24"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/userName"
|
android:id="@+id/avatarImage"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:background="@drawable/bg_avatar_circle"
|
||||||
|
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/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_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:ellipsize="end"
|
android:orientation="vertical"
|
||||||
android:maxLines="1"
|
app:layout_constraintBottom_toBottomOf="@id/avatarContainer"
|
||||||
android:text="用户昵称"
|
|
||||||
android:textColor="#111111"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/addButton"
|
app:layout_constraintEnd_toStartOf="@id/addButton"
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarImage"
|
app:layout_constraintStart_toEndOf="@id/avatarContainer"
|
||||||
app:layout_constraintTop_toTopOf="@id/avatarImage" />
|
app:layout_constraintTop_toTopOf="@id/avatarContainer">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/distanceText"
|
android:id="@+id/userName"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="12dp"
|
android:ellipsize="end"
|
||||||
android:layout_marginTop="4dp"
|
android:maxLines="1"
|
||||||
android:layout_marginEnd="12dp"
|
android:text="用户昵称"
|
||||||
android:ellipsize="end"
|
android:textColor="#1A1A1A"
|
||||||
android:maxLines="1"
|
android:textSize="16sp"
|
||||||
android:text="距离 1.2km"
|
android:textStyle="bold" />
|
||||||
android:textColor="#666666"
|
|
||||||
android:textSize="13sp"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/addButton"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarImage"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/userName" />
|
|
||||||
|
|
||||||
|
<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="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="距离 1.2km"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 添加按钮 -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/addButton"
|
android:id="@+id/addButton"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="32dp"
|
android:layout_height="36dp"
|
||||||
android:background="@drawable/bg_purple_999"
|
android:background="@drawable/bg_add_button_nearby_pressed"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:paddingStart="16dp"
|
android:minWidth="72dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
android:text="添加"
|
android:text="添加"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatarContainer"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="@id/avatarContainer" />
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
## ✅ 已完善功能
|
## ✅ 已完善功能
|
||||||
|
|
||||||
### 1. 主界面 (MainActivity)
|
### 1. 主界面 (MainActivity)
|
||||||
**完成度**: 90%
|
**完成度**: 95%
|
||||||
|
|
||||||
- ✅ 瀑布流布局展示直播间
|
- ✅ 瀑布流布局展示直播间
|
||||||
- ✅ 下拉刷新
|
- ✅ 下拉刷新
|
||||||
|
|
@ -50,16 +50,19 @@
|
||||||
- ✅ 推流地址显示和复制
|
- ✅ 推流地址显示和复制
|
||||||
- ✅ 搜索框UI
|
- ✅ 搜索框UI
|
||||||
- ✅ 语音搜索功能
|
- ✅ 语音搜索功能
|
||||||
- ✅ 分类标签UI
|
- ✅ 分类标签UI和筛选功能(CategoryFilterManager)
|
||||||
|
- ✅ 顶部标签页功能(关注/发现/附近)
|
||||||
- ✅ 底部导航栏
|
- ✅ 底部导航栏
|
||||||
- ✅ 侧边栏菜单
|
- ✅ 侧边栏菜单
|
||||||
|
- ✅ 空状态和错误处理
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 分类筛选功能(UI已准备好,但筛选逻辑使用演示数据)
|
- ⚠️ 分类筛选使用本地数据筛选(等待后端API)
|
||||||
- ⚠️ 顶部标签页(关注/发现/附近)跳转到占位页面
|
- ⚠️ 顶部标签页数据使用演示数据(关注列表、发现推荐、附近用户)
|
||||||
|
- ⚠️ 附近用户功能需要位置权限,数据为模拟数据
|
||||||
|
|
||||||
### 2. 直播间详情 (RoomDetailActivity)
|
### 2. 直播间详情 (RoomDetailActivity)
|
||||||
**完成度**: 85%
|
**完成度**: 90%
|
||||||
|
|
||||||
- ✅ 直播流播放(HLS/FLV)
|
- ✅ 直播流播放(HLS/FLV)
|
||||||
- ✅ 全屏播放
|
- ✅ 全屏播放
|
||||||
|
|
@ -69,12 +72,13 @@
|
||||||
- ✅ 观看人数显示
|
- ✅ 观看人数显示
|
||||||
- ✅ 自动重连机制
|
- ✅ 自动重连机制
|
||||||
- ✅ 低延迟播放配置
|
- ✅ 低延迟播放配置
|
||||||
|
- ✅ 分享功能(系统分享菜单)
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 聊天功能使用模拟数据,未连接真实WebSocket
|
- ⚠️ 聊天功能使用模拟数据,未连接真实WebSocket
|
||||||
- ⚠️ 礼物打赏功能未实现
|
- ⚠️ 礼物打赏功能未实现
|
||||||
- ⚠️ 分享功能未实现
|
|
||||||
- ⚠️ 弹幕功能未实现
|
- ⚠️ 弹幕功能未实现
|
||||||
|
- ⚠️ 分享链接为模拟数据(等待后端生成真实分享链接)
|
||||||
|
|
||||||
### 3. 个人资料 (ProfileActivity)
|
### 3. 个人资料 (ProfileActivity)
|
||||||
**完成度**: 90%
|
**完成度**: 90%
|
||||||
|
|
@ -86,11 +90,13 @@
|
||||||
- ✅ 作品/收藏/赞过标签页
|
- ✅ 作品/收藏/赞过标签页
|
||||||
- ✅ 关注/粉丝/获赞统计
|
- ✅ 关注/粉丝/获赞统计
|
||||||
- ✅ 主页链接复制
|
- ✅ 主页链接复制
|
||||||
|
- ✅ 作品列表基础UI(UserWorksAdapter)
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 作品发布功能(显示"待接入")
|
- ⚠️ 作品发布功能(显示"待接入")
|
||||||
- ⚠️ 头像上传功能(仅支持本地资源)
|
- ⚠️ 头像上传功能(仅支持本地资源)
|
||||||
- ⚠️ 作品列表使用演示数据
|
- ⚠️ 作品列表使用演示数据(Integer列表)
|
||||||
|
- ⚠️ 作品详情页面未实现
|
||||||
|
|
||||||
### 4. 编辑资料 (EditProfileActivity)
|
### 4. 编辑资料 (EditProfileActivity)
|
||||||
**完成度**: 95%
|
**完成度**: 95%
|
||||||
|
|
@ -108,45 +114,53 @@
|
||||||
- ⚠️ 数据同步到后端API
|
- ⚠️ 数据同步到后端API
|
||||||
|
|
||||||
### 5. 消息功能 (MessagesActivity)
|
### 5. 消息功能 (MessagesActivity)
|
||||||
**完成度**: 80%
|
**完成度**: 85%
|
||||||
|
|
||||||
- ✅ 会话列表展示
|
- ✅ 会话列表展示
|
||||||
- ✅ 未读消息徽章
|
- ✅ 未读消息徽章
|
||||||
- ✅ 滑动删除/标记已读
|
- ✅ 滑动删除/标记已读
|
||||||
- ✅ 会话详情页面
|
- ✅ 会话详情页面
|
||||||
- ✅ 消息发送(本地)
|
- ✅ 消息发送(本地)
|
||||||
|
- ✅ 会话搜索功能(按会话标题和消息内容搜索)
|
||||||
|
- ✅ 空状态显示
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 所有消息数据使用演示数据
|
- ⚠️ 所有消息数据使用演示数据
|
||||||
- ⚠️ 未连接真实消息服务器
|
- ⚠️ 未连接真实消息服务器(WebSocket)
|
||||||
- ⚠️ 消息推送功能未实现
|
- ⚠️ 消息推送功能未实现
|
||||||
- ⚠️ 图片/语音消息未实现
|
- ⚠️ 图片/语音消息未实现
|
||||||
|
|
||||||
### 6. 会话详情 (ConversationActivity)
|
### 6. 会话详情 (ConversationActivity)
|
||||||
**完成度**: 75%
|
**完成度**: 90%
|
||||||
|
|
||||||
- ✅ 消息列表展示
|
- ✅ 消息列表展示
|
||||||
- ✅ 消息发送
|
- ✅ 消息发送
|
||||||
- ✅ 自动滚动到底部
|
- ✅ 自动滚动到底部(使用smoothScrollToPosition)
|
||||||
- ✅ 未读消息标记
|
- ✅ 未读消息标记
|
||||||
|
- ✅ 消息状态显示(发送中、已发送、已读)
|
||||||
|
- ✅ 消息长按菜单(复制、删除)
|
||||||
|
- ✅ 内存泄漏防护(onDestroy中清理Handler)
|
||||||
|
- ✅ DiffUtil使用messageId优化性能
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 消息数据使用演示数据
|
- ⚠️ 消息数据使用演示数据
|
||||||
- ⚠️ 未实现实时消息接收
|
- ⚠️ 未实现实时消息接收(WebSocket)
|
||||||
- ⚠️ 消息类型单一(仅文本)
|
- ⚠️ 消息类型单一(仅文本,图片/语音消息未实现)
|
||||||
|
|
||||||
### 7. 搜索功能 (SearchActivity)
|
### 7. 搜索功能 (SearchActivity)
|
||||||
**完成度**: 60%
|
**完成度**: 70%
|
||||||
|
|
||||||
- ✅ 搜索界面UI
|
- ✅ 搜索界面UI
|
||||||
- ✅ 实时搜索过滤
|
- ✅ 实时搜索过滤
|
||||||
- ✅ 搜索结果展示
|
- ✅ 搜索结果展示
|
||||||
|
- ✅ 空状态显示(无搜索结果时显示提示)
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 仅支持本地数据过滤
|
- ⚠️ 仅支持本地数据过滤
|
||||||
- ⚠️ 未连接后端搜索API
|
- ⚠️ 未连接后端搜索API
|
||||||
- ⚠️ 搜索历史未实现
|
- ⚠️ 搜索历史未实现
|
||||||
- ⚠️ 热门搜索未实现
|
- ⚠️ 热门搜索未实现
|
||||||
|
- ⚠️ 搜索建议(自动补全)未实现
|
||||||
|
|
||||||
### 8. 缘池功能 (FishPondActivity)
|
### 8. 缘池功能 (FishPondActivity)
|
||||||
**完成度**: 85%
|
**完成度**: 85%
|
||||||
|
|
@ -174,42 +188,47 @@
|
||||||
|
|
||||||
### 10. 其他已实现页面
|
### 10. 其他已实现页面
|
||||||
- ✅ 观看历史 (WatchHistoryActivity) - 基础UI
|
- ✅ 观看历史 (WatchHistoryActivity) - 基础UI
|
||||||
- ✅ 我的好友 (MyFriendsActivity) - 基础UI
|
- ✅ 我的好友 (MyFriendsActivity) - 基础UI,支持空状态
|
||||||
- ✅ 粉丝列表 (FansListActivity) - 基础UI
|
- ✅ 粉丝列表 (FansListActivity) - 基础UI
|
||||||
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
||||||
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
||||||
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
|
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
|
||||||
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
||||||
|
- ✅ 登录页面 (LoginActivity) - 完整实现
|
||||||
|
- ✅ 注册页面 (RegisterActivity) - 完整实现
|
||||||
|
- ✅ 通知列表 (NotificationsActivity) - 完整实现,支持分类筛选
|
||||||
|
- ✅ 通知设置 (NotificationSettingsActivity) - 完整实现
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ 部分完善功能
|
## ⚠️ 部分完善功能
|
||||||
|
|
||||||
### 1. 网络层集成
|
### 1. 网络层集成
|
||||||
**完成度**: 60%
|
**完成度**: 70%
|
||||||
|
|
||||||
- ✅ Retrofit + OkHttp 配置完成
|
- ✅ Retrofit + OkHttp 配置完成
|
||||||
- ✅ API 接口定义完成
|
- ✅ API 接口定义完成
|
||||||
- ✅ 基础错误处理
|
- ✅ 基础错误处理(ErrorHandler工具类)
|
||||||
- ✅ 模拟器/真机网络地址配置
|
- ✅ 模拟器/真机网络地址配置
|
||||||
|
- ✅ 网络请求生命周期管理(NetworkRequestManager)
|
||||||
|
- ✅ 统一错误提示和重试机制
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ API 调用失败时大量使用演示数据
|
- ⚠️ API 调用失败时大量使用演示数据
|
||||||
- ⚠️ 缺少统一的错误处理机制
|
|
||||||
- ⚠️ 缺少请求重试机制
|
- ⚠️ 缺少请求重试机制
|
||||||
- ⚠️ 缺少网络状态监听
|
- ⚠️ 缺少网络状态监听
|
||||||
- ⚠️ 缺少请求缓存策略
|
- ⚠️ 缺少请求缓存策略
|
||||||
|
|
||||||
### 2. 数据持久化
|
### 2. 数据存储(前端)
|
||||||
**完成度**: 50%
|
**完成度**: 60%
|
||||||
|
|
||||||
- ✅ SharedPreferences 用于个人资料
|
- ✅ SharedPreferences 用于个人资料和配置
|
||||||
- ✅ 本地数据缓存
|
- ✅ 本地数据缓存(内存缓存)
|
||||||
|
- ✅ 分类筛选条件记忆(SharedPreferences)
|
||||||
|
|
||||||
**待完善**:
|
**待完善**:
|
||||||
- ⚠️ 未使用 Room 数据库
|
- ⚠️ 未实现离线数据支持(当前仅使用内存缓存)
|
||||||
- ⚠️ 未实现离线数据支持
|
- ⚠️ 未实现数据同步机制(等待后端)
|
||||||
- ⚠️ 未实现数据同步机制
|
|
||||||
|
|
||||||
### 3. 图片处理
|
### 3. 图片处理
|
||||||
**完成度**: 70%
|
**完成度**: 70%
|
||||||
|
|
@ -230,12 +249,12 @@
|
||||||
### 1. 后端API集成
|
### 1. 后端API集成
|
||||||
**问题**: 虽然定义了API接口,但很多功能在API失败时回退到演示数据
|
**问题**: 虽然定义了API接口,但很多功能在API失败时回退到演示数据
|
||||||
|
|
||||||
**需要完善**:
|
**需要完善**(前端部分):
|
||||||
- [ ] 完善所有API接口的错误处理
|
- [ ] 完善所有API接口的错误处理(前端错误提示)
|
||||||
- [ ] 实现API请求的加载状态管理
|
- [ ] 实现API请求的加载状态管理(前端UI)
|
||||||
- [ ] 实现API数据的本地缓存
|
- [ ] 添加API版本管理(前端配置)
|
||||||
- [ ] 实现数据同步机制
|
|
||||||
- [ ] 添加API版本管理
|
**注意**: API数据的本地缓存和数据同步机制需要等待后端支持,前端仅负责UI展示和错误处理。
|
||||||
|
|
||||||
### 2. 实时通信
|
### 2. 实时通信
|
||||||
**问题**: 聊天、弹幕等功能使用模拟数据
|
**问题**: 聊天、弹幕等功能使用模拟数据
|
||||||
|
|
@ -276,20 +295,30 @@
|
||||||
- [ ] 添加位置权限处理
|
- [ ] 添加位置权限处理
|
||||||
|
|
||||||
### 6. 占位页面功能
|
### 6. 占位页面功能
|
||||||
**以下功能跳转到 `TabPlaceholderActivity`,需要实现**:
|
**状态**: 部分功能已实现基础UI,但核心功能未实现
|
||||||
|
|
||||||
- [ ] 语音匹配 (VoiceMatchActivity)
|
**已实现基础UI的Activity**:
|
||||||
- [ ] 心动信号 (HeartbeatSignalActivity)
|
- ✅ 语音匹配 (VoiceMatchActivity) - 基础页面
|
||||||
- [ ] 在线处对象 (OnlineDatingActivity)
|
- ✅ 心动信号 (HeartbeatSignalActivity) - 基础页面
|
||||||
- [ ] 找人玩游戏 (FindGameActivity)
|
- ✅ 在线处对象 (OnlineDatingActivity) - 基础页面
|
||||||
- [ ] 一起KTV (KTVTogetherActivity)
|
- ✅ 找人玩游戏 (FindGameActivity) - 基础页面
|
||||||
- [ ] 你画我猜 (DrawGuessActivity)
|
- ✅ 一起KTV (KTVTogetherActivity) - 基础页面
|
||||||
- [ ] 和平精英 (PeaceEliteActivity)
|
- ✅ 你画我猜 (DrawGuessActivity) - 基础页面
|
||||||
- [ ] 桌子游 (TableGamesActivity)
|
- ✅ 和平精英 (PeaceEliteActivity) - 基础页面
|
||||||
- [ ] 公园勋章
|
- ✅ 桌子游 (TableGamesActivity) - 基础页面
|
||||||
- [ ] 分享主页(部分实现,仅复制链接)
|
|
||||||
- [ ] 加好友
|
**TabPlaceholderActivity中的占位功能**:
|
||||||
- [ ] 定位/发现
|
- ⚠️ 公园勋章 - 显示占位页面
|
||||||
|
- ⚠️ 加好友 - 显示附近用户列表(演示数据)
|
||||||
|
- ⚠️ 定位/发现 - 显示占位页面
|
||||||
|
- ⚠️ 关注页面(TabPlaceholderActivity中) - 显示关注列表(演示数据)
|
||||||
|
- ⚠️ 附近页面(TabPlaceholderActivity中) - 显示附近用户(演示数据)
|
||||||
|
- ⚠️ 发现页面(TabPlaceholderActivity中) - 显示推荐内容(演示数据)
|
||||||
|
|
||||||
|
**需要完善**:
|
||||||
|
- [ ] 实现各功能的核心逻辑
|
||||||
|
- [ ] 接入后端API获取真实数据
|
||||||
|
- [ ] 实现游戏匹配、语音匹配等功能
|
||||||
|
|
||||||
### 7. 作品功能
|
### 7. 作品功能
|
||||||
**问题**: 个人资料中的"作品"标签页未实现
|
**问题**: 个人资料中的"作品"标签页未实现
|
||||||
|
|
@ -384,11 +413,12 @@
|
||||||
- [ ] 引入依赖注入框架(Hilt/Dagger)
|
- [ ] 引入依赖注入框架(Hilt/Dagger)
|
||||||
- [ ] 模块化拆分
|
- [ ] 模块化拆分
|
||||||
|
|
||||||
### 2. 数据层优化
|
### 2. 数据层优化(前端)
|
||||||
- [ ] 引入 Room 数据库
|
- [ ] 实现 Repository 模式(本地数据源)
|
||||||
- [ ] 实现 Repository 模式
|
- [ ] 优化内存缓存策略
|
||||||
- [ ] 添加数据同步机制
|
- [ ] 实现请求去重机制
|
||||||
- [ ] 实现离线数据支持
|
|
||||||
|
**注意**: Room数据库和离线数据支持暂不实现,当前使用SharedPreferences和内存缓存。
|
||||||
|
|
||||||
### 3. UI/UX 优化
|
### 3. UI/UX 优化
|
||||||
- [ ] 统一设计语言和组件库
|
- [ ] 统一设计语言和组件库
|
||||||
|
|
@ -439,48 +469,29 @@
|
||||||
- [x] 修复动画未取消问题
|
- [x] 修复动画未取消问题
|
||||||
- [x] 完善网络请求取消机制
|
- [x] 完善网络请求取消机制
|
||||||
- [x] 优化播放器资源释放
|
- [x] 优化播放器资源释放
|
||||||
- [x] 添加 LeakCanary 检测
|
|
||||||
|
|
||||||
**实际工作量**: 1.5天
|
|
||||||
**完成内容**:
|
**完成内容**:
|
||||||
- 为所有使用Handler的Activity添加onDestroy方法,清理Runnable防止内存泄漏
|
- 为所有使用Handler的Activity添加onDestroy方法,清理Runnable防止内存泄漏
|
||||||
- 完善动画生命周期管理,确保动画在Activity销毁时正确取消
|
- 完善动画生命周期管理,确保动画在Activity销毁时正确取消
|
||||||
- 创建NetworkRequestManager和NetworkUtils,实现生命周期感知的网络请求管理
|
- 创建`NetworkRequestManager`工具类,实现生命周期感知的网络请求管理,在Activity销毁时自动取消所有请求
|
||||||
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
||||||
- 集成LeakCanary内存泄漏检测工具
|
|
||||||
|
|
||||||
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
|
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
|
||||||
**为什么重要**: 提升用户体验一致性
|
**为什么重要**: 提升用户体验一致性
|
||||||
|
|
||||||
- [x] 创建统一的加载状态组件
|
- [x] 创建统一的加载状态管理器
|
||||||
- [x] 实现骨架屏(Skeleton Screen)
|
- [x] 实现骨架屏(Skeleton Screen)
|
||||||
- [x] 统一所有页面的加载提示
|
- [x] 统一所有页面的加载提示
|
||||||
- [x] 添加加载动画
|
|
||||||
|
|
||||||
**完成内容**:
|
**完成内容**:
|
||||||
- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
|
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持骨架屏、下拉刷新等)
|
||||||
- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
|
- 实现了骨架屏适配器(`SkeletonAdapter`),在RecyclerView中显示占位效果
|
||||||
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
|
- 更新了主要Activity使用统一的加载状态:
|
||||||
- 更新了所有主要Activity使用统一的加载状态:
|
- **MainActivity**: 在列表为空时使用骨架屏显示加载状态
|
||||||
- **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
|
- **SearchActivity**: 搜索时显示加载状态
|
||||||
- **RoomDetailActivity**: 使用LoadingView显示加载状态
|
|
||||||
- **SearchActivity**: 搜索时显示"搜索中..."加载状态
|
|
||||||
- **MessagesActivity**: 加载消息列表时显示加载状态
|
- **MessagesActivity**: 加载消息列表时显示加载状态
|
||||||
- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
|
|
||||||
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
|
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
|
||||||
|
|
||||||
#### 4. **数据持久化(本地)** ⭐⭐⭐
|
|
||||||
**为什么重要**: 支持离线使用,提升用户体验
|
|
||||||
|
|
||||||
- [ ] 引入 Room 数据库
|
|
||||||
- [ ] 缓存直播间列表到本地
|
|
||||||
- [ ] 缓存用户信息
|
|
||||||
- [ ] 实现搜索历史存储
|
|
||||||
- [ ] 实现观看历史存储
|
|
||||||
- [ ] 实现消息记录缓存
|
|
||||||
|
|
||||||
**预计工作量**: 3-5天
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🟡 第二优先级(功能完善与架构优化)
|
### 🟡 第二优先级(功能完善与架构优化)
|
||||||
|
|
@ -501,7 +512,7 @@
|
||||||
|
|
||||||
- [x] 完善本地数据筛选逻辑
|
- [x] 完善本地数据筛选逻辑
|
||||||
- [x] 实现筛选条件记忆(SharedPreferences)
|
- [x] 实现筛选条件记忆(SharedPreferences)
|
||||||
- [x] 优化筛选性能
|
- [x] 优化筛选性能(异步筛选)
|
||||||
- [x] 添加筛选动画效果
|
- [x] 添加筛选动画效果
|
||||||
|
|
||||||
**完成内容**:
|
**完成内容**:
|
||||||
|
|
@ -511,55 +522,88 @@
|
||||||
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
|
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
|
||||||
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
|
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
|
||||||
- 在Activity恢复时自动恢复上次选中的分类标签
|
- 在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(会话列表)
|
- [x] 完善消息列表UI(会话列表)
|
||||||
- [ ] 实现消息状态显示(发送中、已发送、已读)
|
- [x] 实现消息状态显示(发送中、已发送、已读)
|
||||||
- [ ] 优化消息列表性能(使用messageId)
|
- [x] 优化消息列表性能(使用messageId)
|
||||||
- [ ] 添加消息操作(复制、删除等)
|
- [x] 添加消息操作(复制、删除等)
|
||||||
- [ ] 实现消息搜索(本地)
|
- [x] 实现消息搜索(本地)
|
||||||
|
|
||||||
**已完成内容**:
|
**已完成内容**:
|
||||||
- ✅ 会话列表展示(MessagesActivity)
|
- ✅ 会话列表展示(MessagesActivity)
|
||||||
- ✅ 滑动删除/标记已读功能(MessagesActivity)
|
- ✅ 滑动删除/标记已读功能(MessagesActivity)
|
||||||
- ✅ 消息发送和列表展示(ConversationActivity)
|
- ✅ 消息发送和列表展示(ConversationActivity)
|
||||||
- ✅ 未读消息徽章管理
|
- ✅ 未读消息徽章管理
|
||||||
|
- ✅ ChatMessage已添加MessageStatus枚举(SENDING, SENT, READ)
|
||||||
|
- ✅ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)- ConversationMessagesAdapter已实现
|
||||||
|
- ✅ 消息长按菜单,支持复制和删除操作 - ConversationActivity已实现
|
||||||
|
- ✅ DiffUtil已使用messageId作为唯一标识,提升列表更新性能 - ConversationMessagesAdapter已优化
|
||||||
|
- ✅ MessagesActivity已实现搜索功能,支持按会话标题和消息内容搜索
|
||||||
|
- ✅ 内存泄漏防护已实现(onDestroy中清理Handler延迟任务)- ConversationActivity已实现
|
||||||
|
- ✅ 滚动行为已优化(使用smoothScrollToPosition)- ConversationActivity已实现
|
||||||
|
|
||||||
**待实现内容**:
|
**待完善内容**:
|
||||||
- ⚠️ 为ChatMessage添加MessageStatus枚举(发送中、已发送、已读)
|
- ⚠️ 消息预览功能(处理图片/语音消息类型)- 当前仅支持文本消息
|
||||||
- ⚠️ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
- ⚠️ 图片/语音消息发送和接收功能
|
||||||
- ⚠️ 实现消息长按菜单,支持复制和删除操作
|
- ⚠️ 实时消息接收(WebSocket集成,等待后端)
|
||||||
- ⚠️ 优化DiffUtil,使用messageId作为唯一标识,提升列表更新性能(当前使用timestamp)
|
|
||||||
- ⚠️ 在MessagesActivity中添加搜索功能,支持按会话标题和消息内容搜索
|
|
||||||
- ⚠️ 优化消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
|
||||||
- ⚠️ 添加内存泄漏防护(onDestroy中清理延迟任务)
|
|
||||||
- ⚠️ 优化滚动行为(使用smoothScrollToPosition替代scrollToPosition)
|
|
||||||
|
|
||||||
**预计工作量**: 3-4天
|
**预计工作量**: 3-4天(已完成)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -609,15 +653,30 @@
|
||||||
|
|
||||||
### 🔵 第四优先级(功能扩展)
|
### 🔵 第四优先级(功能扩展)
|
||||||
|
|
||||||
#### 14. **作品功能(前端UI)** ⭐
|
#### 14. **作品功能(前端UI)** ⭐ ⚠️ 部分完成
|
||||||
**为什么重要**: 完善个人中心功能
|
**为什么重要**: 完善个人中心功能
|
||||||
|
|
||||||
- [ ] 实现作品列表UI
|
- [x] 作品列表UI(ProfileActivity已实现基础UI)
|
||||||
|
- [x] UserWorksAdapter已创建(使用演示数据)
|
||||||
- [ ] 实现作品发布UI(数据仅本地存储)
|
- [ ] 实现作品发布UI(数据仅本地存储)
|
||||||
- [ ] 实现作品详情页面
|
- [ ] 实现作品详情页面
|
||||||
- [ ] 实现作品编辑UI
|
- [ ] 实现作品编辑UI
|
||||||
|
- [ ] 作品数据模型(WorkItem)
|
||||||
|
|
||||||
**预计工作量**: 3-4天
|
**已完成内容**:
|
||||||
|
- ✅ ProfileActivity中已实现作品标签页UI
|
||||||
|
- ✅ UserWorksAdapter已创建,支持显示作品列表
|
||||||
|
- ✅ 作品列表使用GridLayoutManager(3列布局)
|
||||||
|
- ✅ 支持作品/收藏/赞过三个标签页切换
|
||||||
|
|
||||||
|
**待实现内容**:
|
||||||
|
- ⚠️ 作品发布功能(当前显示"待接入"提示)
|
||||||
|
- ⚠️ 作品详情页面(点击作品查看详情)
|
||||||
|
- ⚠️ 作品编辑和删除功能
|
||||||
|
- ⚠️ 作品数据模型(WorkItem),当前使用Integer列表(drawable资源ID)作为临时方案
|
||||||
|
- ⚠️ 作品数据持久化(本地存储,等待后端API)
|
||||||
|
|
||||||
|
**预计工作量**: 3-4天(部分完成,剩余2-3天)
|
||||||
|
|
||||||
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
|
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
|
||||||
**为什么重要**: 提升传播能力
|
**为什么重要**: 提升传播能力
|
||||||
|
|
@ -698,17 +757,6 @@
|
||||||
- [ ] 重构重复代码
|
- [ ] 重构重复代码
|
||||||
|
|
||||||
**预计工作量**: 持续进行
|
**预计工作量**: 持续进行
|
||||||
|
|
||||||
#### 20. **测试** ⭐
|
|
||||||
**为什么重要**: 保证代码质量
|
|
||||||
|
|
||||||
- [ ] 编写单元测试(工具类)
|
|
||||||
- [ ] 编写 UI 测试(关键流程)
|
|
||||||
- [ ] 性能测试
|
|
||||||
- [ ] 兼容性测试
|
|
||||||
|
|
||||||
**预计工作量**: 持续进行
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📋 暂不实现(需等待后端)
|
### 📋 暂不实现(需等待后端)
|
||||||
|
|
@ -722,22 +770,23 @@
|
||||||
- ❌ 支付功能(等待后端和支付SDK)
|
- ❌ 支付功能(等待后端和支付SDK)
|
||||||
- ❌ 推流功能(如需要,等待推流SDK集成)
|
- ❌ 推流功能(如需要,等待推流SDK集成)
|
||||||
|
|
||||||
|
**注意**: 数据持久化(Room数据库)相关功能暂不实现,当前使用SharedPreferences存储简单配置数据。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📅 开发时间估算
|
## 📅 开发时间估算
|
||||||
|
|
||||||
### 第一阶段(核心修复)- 约 8-13 天
|
### 第一阶段(核心修复)- 约 5-8 天
|
||||||
- 空状态和错误处理
|
- 空状态和错误处理
|
||||||
- 生命周期和内存管理
|
- 生命周期和内存管理
|
||||||
- 统一加载状态
|
- 统一加载状态
|
||||||
- 数据持久化
|
|
||||||
|
|
||||||
### 第二阶段(架构优化)- 约 12-18 天
|
### 第二阶段(架构优化)- 约 8-12 天
|
||||||
- 前端架构优化
|
- 前端架构优化
|
||||||
- 分类筛选功能
|
- ~~分类筛选功能~~ ✅ 已完成
|
||||||
- 顶部标签页功能
|
- ~~顶部标签页功能~~ ✅ 已完成
|
||||||
- 搜索功能增强
|
- 搜索功能增强(部分完成,剩余搜索历史和热门搜索)
|
||||||
- 消息功能完善
|
- ~~消息功能完善~~ ✅ 已完成
|
||||||
|
|
||||||
### 第三阶段(体验增强)- 约 9-13 天
|
### 第三阶段(体验增强)- 约 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周**: 空状态 + 错误处理 + 内存泄漏修复
|
1. **第1周**: 空状态 + 错误处理 + 内存泄漏修复
|
||||||
2. **第2周**: 统一加载状态 + 数据持久化(Room)
|
2. **第2周**: 统一加载状态 + 分类筛选
|
||||||
3. **第3周**: 架构优化(MVVM)+ 分类筛选
|
3. **第3周**: 架构优化(MVVM)+ 顶部标签页
|
||||||
4. **第4周**: 顶部标签页 + 搜索增强
|
4. **第4周**: 搜索增强 + 消息功能完善
|
||||||
|
|
||||||
这样可以在一个月内完成核心功能的前端完善。
|
这样可以在一个月内完成核心功能的前端完善。
|
||||||
|
|
||||||
|
|
@ -776,18 +827,20 @@
|
||||||
## 📝 总结(前端开发视角)
|
## 📝 总结(前端开发视角)
|
||||||
|
|
||||||
### 当前状态
|
### 当前状态
|
||||||
- **整体完成度**: 约 60-70%(前端UI层面)
|
- **整体完成度**: 约 80-85%(前端UI层面)
|
||||||
- **核心UI功能**: 基本完成,但缺少完善
|
- **核心UI功能**: 基本完成,大部分功能已实现前端逻辑
|
||||||
- **用户体验**: 良好,但需要优化细节
|
- **用户体验**: 良好,已实现空状态、错误处理、加载状态等
|
||||||
- **代码质量**: 中等,需要重构和优化
|
- **代码质量**: 中等,已实现部分工具类和统一管理
|
||||||
- **数据层**: 仅使用 SharedPreferences,需要引入 Room
|
- **数据层**: 使用 SharedPreferences 存储配置,内存缓存存储临时数据
|
||||||
|
- **已完成功能**: 主界面(含顶部标签页、分类筛选)、直播间详情、个人资料、消息(含状态显示、长按菜单、搜索)、搜索(含实时过滤、空状态)、分享、通知、设置等
|
||||||
|
- **最近完成**: 消息功能完善(状态显示、长按菜单、性能优化、搜索功能)
|
||||||
|
|
||||||
### 前端可独立完成的工作
|
### 前端可独立完成的工作
|
||||||
1. ✅ **UI/UX 完善**: 空状态、错误处理、加载状态
|
1. ✅ **UI/UX 完善**: 空状态、错误处理、加载状态
|
||||||
2. ✅ **架构优化**: MVVM、Repository 模式、代码重构
|
2. ✅ **架构优化**: MVVM、Repository 模式、代码重构
|
||||||
3. ✅ **数据持久化**: Room 数据库、本地缓存
|
3. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
|
||||||
4. ✅ **功能完善**: 分类筛选、搜索增强、消息功能
|
4. ✅ **体验增强**: 引导页、动画、深色模式
|
||||||
5. ✅ **体验增强**: 引导页、动画、深色模式
|
5. ✅ **工具类**: 分享功能、通知功能、设置页面、缓存管理
|
||||||
|
|
||||||
### 需要等待后端的功能
|
### 需要等待后端的功能
|
||||||
1. ❌ **API 集成**: 等待后端接口定义
|
1. ❌ **API 集成**: 等待后端接口定义
|
||||||
|
|
@ -798,9 +851,9 @@
|
||||||
### 前端开发建议
|
### 前端开发建议
|
||||||
1. **立即开始**: 空状态处理、内存泄漏修复、统一加载状态
|
1. **立即开始**: 空状态处理、内存泄漏修复、统一加载状态
|
||||||
2. **第一周**: 完成基础架构和用户体验修复
|
2. **第一周**: 完成基础架构和用户体验修复
|
||||||
3. **第二周**: 引入 Room 数据库,实现本地数据持久化
|
3. **第二周**: 架构优化,引入 MVVM 模式
|
||||||
4. **第三周**: 架构优化,引入 MVVM 模式
|
4. **第三周**: 功能完善,实现分类筛选、搜索增强等
|
||||||
5. **第四周**: 功能完善,实现分类筛选、搜索增强等
|
5. **第四周**: 体验增强,实现引导页、动画优化等
|
||||||
|
|
||||||
### 开发原则
|
### 开发原则
|
||||||
- ✅ **优先前端可独立完成的工作**
|
- ✅ **优先前端可独立完成的工作**
|
||||||
|
|
@ -808,6 +861,7 @@
|
||||||
- ✅ **保持代码结构便于后续对接后端**
|
- ✅ **保持代码结构便于后续对接后端**
|
||||||
- ✅ **注重用户体验和代码质量**
|
- ✅ **注重用户体验和代码质量**
|
||||||
- ⏸️ **等待后端接口的功能先做UI,数据用模拟**
|
- ⏸️ **等待后端接口的功能先做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