feat: 添加昵称设置功能
- 在AppNavigation中新增NicknameSetup页面 - 更新AuthService以支持昵称更新请求 - 在AuthViewModel中添加昵称设置逻辑 - 优化LoginScreen以处理首次登录需要设置昵称的情况 - 更新ConversationListScreen以支持自动创建对话的状态管理 - 新增NicknameSetupScreen组件用于用户设置昵称
This commit is contained in:
@@ -18,6 +18,7 @@ sealed class Screen(val route: String) {
|
|||||||
object Profile : Screen("profile")
|
object Profile : Screen("profile")
|
||||||
object PersonalInfo : Screen("personal_info")
|
object PersonalInfo : Screen("personal_info")
|
||||||
object Login : Screen("login")
|
object Login : Screen("login")
|
||||||
|
object NicknameSetup : Screen("nickname_setup")
|
||||||
object ResetPassword : Screen("reset_password")
|
object ResetPassword : Screen("reset_password")
|
||||||
object AccountManagement : Screen("account_management")
|
object AccountManagement : Screen("account_management")
|
||||||
object UpgradePlan : Screen("upgrade_plan")
|
object UpgradePlan : Screen("upgrade_plan")
|
||||||
@@ -199,6 +200,13 @@ fun AppNavigation(
|
|||||||
popUpTo(Screen.Login.route) { inclusive = true }
|
popUpTo(Screen.Login.route) { inclusive = true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onLoginSuccessNeedsNickname = {
|
||||||
|
// 登录成功但需要设置昵称,导航到昵称设置页面
|
||||||
|
navController.navigate(Screen.NicknameSetup.route) {
|
||||||
|
// 清除登录页面,避免返回
|
||||||
|
popUpTo(Screen.Login.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
onNavigateToResetPassword = {
|
onNavigateToResetPassword = {
|
||||||
navController.navigate(Screen.ResetPassword.route)
|
navController.navigate(Screen.ResetPassword.route)
|
||||||
},
|
},
|
||||||
@@ -210,6 +218,23 @@ fun AppNavigation(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = Screen.NicknameSetup.route,
|
||||||
|
enterTransition = { fadeInTransition() },
|
||||||
|
exitTransition = { fadeOutTransition() },
|
||||||
|
popEnterTransition = { slideInHorizontallyFromLeft() },
|
||||||
|
popExitTransition = { slideOutHorizontallyToRight() }
|
||||||
|
) {
|
||||||
|
NicknameSetupScreen(
|
||||||
|
onSetupComplete = {
|
||||||
|
// 昵称设置完成后导航到主界面
|
||||||
|
navController.navigate(Screen.ConversationList.route) {
|
||||||
|
// 清除昵称设置页面和登录页面,避免返回
|
||||||
|
popUpTo(Screen.Login.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Screen.ResetPassword.route,
|
route = Screen.ResetPassword.route,
|
||||||
enterTransition = { slideInHorizontally() },
|
enterTransition = { slideInHorizontally() },
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import io.ktor.client.request.forms.*
|
|||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.put
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpHeaders
|
import io.ktor.http.HttpHeaders
|
||||||
@@ -526,4 +527,40 @@ class AuthService {
|
|||||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户昵称
|
||||||
|
*/
|
||||||
|
suspend fun updateNickname(accessToken: String, nickname: String): Result<UserResponse> {
|
||||||
|
return try {
|
||||||
|
val response = client.put("$AUTH_BASE/me/nickname") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
header("Authorization", "Bearer $accessToken")
|
||||||
|
setBody(UpdateNicknameRequest(nickname))
|
||||||
|
}
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
val userResponse = response.body<UserResponse>()
|
||||||
|
Result.success(userResponse)
|
||||||
|
}
|
||||||
|
HttpStatusCode.BadRequest -> {
|
||||||
|
try {
|
||||||
|
val error = response.body<ErrorResponse>()
|
||||||
|
Result.failure(Exception(error.detail))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("更新昵称失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HttpStatusCode.Unauthorized -> {
|
||||||
|
Result.failure(Exception("未授权"))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Result.failure(Exception("更新昵称失败: ${response.status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,3 +136,9 @@ data class ChangePhoneRequest(
|
|||||||
data class MessageResponse(
|
data class MessageResponse(
|
||||||
val message: String
|
val message: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 更新昵称请求
|
||||||
|
@Serializable
|
||||||
|
data class UpdateNicknameRequest(
|
||||||
|
val nickname: String
|
||||||
|
)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ object AppIcons {
|
|||||||
val Phone = Icons.Default.Phone
|
val Phone = Icons.Default.Phone
|
||||||
val ExitToApp = Icons.AutoMirrored.Filled.ExitToApp
|
val ExitToApp = Icons.AutoMirrored.Filled.ExitToApp
|
||||||
val DevicesOther = Icons.Default.DevicesOther
|
val DevicesOther = Icons.Default.DevicesOther
|
||||||
|
val PersonAdd = Icons.Default.PersonAdd
|
||||||
|
|
||||||
// 错误处理图标
|
// 错误处理图标
|
||||||
val WifiOff = Icons.Default.WifiOff
|
val WifiOff = Icons.Default.WifiOff
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ fun ConversationListScreen(
|
|||||||
var isSelectionMode by remember { mutableStateOf(false) }
|
var isSelectionMode by remember { mutableStateOf(false) }
|
||||||
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
|
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
|
||||||
|
|
||||||
|
// 是否正在自动创建对话
|
||||||
|
var isAutoCreating by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 刷新对话列表
|
// 刷新对话列表
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.refreshConversations()
|
viewModel.refreshConversations()
|
||||||
@@ -72,16 +75,18 @@ fun ConversationListScreen(
|
|||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// 处理新建对话
|
// 当对话列表为空时,自动创建一个对话并进入
|
||||||
val handleCreateConversation: () -> Unit = {
|
LaunchedEffect(conversations, isLoading, isAutoCreating) {
|
||||||
scope.launch {
|
if (!isLoading && conversations.isEmpty() && !isAutoCreating) {
|
||||||
|
isAutoCreating = true
|
||||||
val result = viewModel.createConversation()
|
val result = viewModel.createConversation()
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { conversationId ->
|
onSuccess = { conversationId ->
|
||||||
onConversationClick(conversationId)
|
onConversationClick(conversationId)
|
||||||
},
|
},
|
||||||
onFailure = { exception ->
|
onFailure = { exception ->
|
||||||
// 错误处理可以在这里添加
|
// 创建失败,重置状态
|
||||||
|
isAutoCreating = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -175,14 +180,7 @@ fun ConversationListScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 正常模式下的操作
|
// 正常模式下不显示任何操作按钮(禁止创建多个对话)
|
||||||
IconButton(onClick = handleCreateConversation) {
|
|
||||||
Icon(
|
|
||||||
imageVector = AppIcons.Add,
|
|
||||||
contentDescription = "新建对话",
|
|
||||||
tint = LightPurple
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
@@ -210,13 +208,17 @@ fun ConversationListScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
conversations.isEmpty() -> {
|
conversations.isEmpty() -> {
|
||||||
// 空状态 - 提示用户创建新对话
|
// 空状态 - 正在自动创建对话
|
||||||
|
if (isAutoCreating) {
|
||||||
|
LoadingIndicator()
|
||||||
|
} else {
|
||||||
EmptyStateView(
|
EmptyStateView(
|
||||||
title = "还没有对话",
|
title = "正在初始化",
|
||||||
message = "点击右上角「+」按钮开始您的回忆录之旅",
|
message = "正在为您准备回忆录对话...",
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -243,16 +245,20 @@ fun ConversationListScreen(
|
|||||||
onConversationClick(conversation.id)
|
onConversationClick(conversation.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDelete = {
|
// 只有一个对话时禁止删除
|
||||||
|
onDelete = if (conversations.size > 1) {
|
||||||
|
{
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.deleteConversation(conversation.id)
|
viewModel.deleteConversation(conversation.id)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
isSelected = selectedIds.contains(conversation.id),
|
|
||||||
isSelectionMode = isSelectionMode,
|
|
||||||
onLongClick = {
|
|
||||||
handleLongClick(conversation.id)
|
|
||||||
}
|
}
|
||||||
|
} else null,
|
||||||
|
isSelected = selectedIds.contains(conversation.id),
|
||||||
|
// 只有一个对话时禁止多选模式
|
||||||
|
isSelectionMode = isSelectionMode && conversations.size > 1,
|
||||||
|
onLongClick = if (conversations.size > 1) {
|
||||||
|
{ handleLongClick(conversation.id) }
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.huaga.life_echo.ui.components.auth.SmsCodeInput
|
|||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
onLoginSuccess: () -> Unit,
|
onLoginSuccess: () -> Unit,
|
||||||
|
onLoginSuccessNeedsNickname: () -> Unit = {},
|
||||||
onNavigateToResetPassword: (() -> Unit)? = null,
|
onNavigateToResetPassword: (() -> Unit)? = null,
|
||||||
onNavigateToTerms: () -> Unit = {},
|
onNavigateToTerms: () -> Unit = {},
|
||||||
onNavigateToPrivacy: () -> Unit = {},
|
onNavigateToPrivacy: () -> Unit = {},
|
||||||
@@ -44,8 +45,6 @@ fun LoginScreen(
|
|||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
var smsCode by remember { mutableStateOf("") }
|
var smsCode by remember { mutableStateOf("") }
|
||||||
var nickname by remember { mutableStateOf("") } // 首次登录时的昵称
|
|
||||||
var needsNickname by remember { mutableStateOf(false) } // 是否需要输入昵称
|
|
||||||
var agreedToTerms by remember { mutableStateOf(false) }
|
var agreedToTerms by remember { mutableStateOf(false) }
|
||||||
var showResultDialog by remember { mutableStateOf(false) }
|
var showResultDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -55,28 +54,29 @@ fun LoginScreen(
|
|||||||
val operationResult by viewModel.operationResult.collectAsState()
|
val operationResult by viewModel.operationResult.collectAsState()
|
||||||
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
|
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
|
||||||
val smsCountdown by viewModel.smsCountdown.collectAsState()
|
val smsCountdown by viewModel.smsCountdown.collectAsState()
|
||||||
|
val needsNicknameSetup by viewModel.needsNicknameSetup.collectAsState()
|
||||||
|
|
||||||
// 显示操作结果弹窗,并检查是否需要输入昵称
|
// 显示操作结果弹窗
|
||||||
LaunchedEffect(operationResult) {
|
LaunchedEffect(operationResult) {
|
||||||
operationResult?.let {
|
operationResult?.let {
|
||||||
showResultDialog = true
|
showResultDialog = true
|
||||||
// 检查是否是首次登录需要昵称
|
|
||||||
if (!it.success && (it.details?.contains("首次登录") == true ||
|
|
||||||
it.details?.contains("需要设置昵称") == true)) {
|
|
||||||
needsNickname = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功后导航(延迟一下,让用户看到成功提示)
|
// 登录成功后导航(延迟一下,让用户看到成功提示)
|
||||||
LaunchedEffect(isLoggedIn, successMessage) {
|
LaunchedEffect(isLoggedIn, successMessage, needsNicknameSetup) {
|
||||||
if (isLoggedIn && successMessage != null) {
|
if (isLoggedIn && successMessage != null) {
|
||||||
kotlinx.coroutines.delay(1500) // 显示1.5秒成功提示后跳转
|
kotlinx.coroutines.delay(1500) // 显示1.5秒成功提示后跳转
|
||||||
viewModel.clearSuccess()
|
viewModel.clearSuccess()
|
||||||
viewModel.clearOperationResult()
|
viewModel.clearOperationResult()
|
||||||
|
// 根据是否需要设置昵称决定导航目标
|
||||||
|
if (needsNicknameSetup) {
|
||||||
|
onLoginSuccessNeedsNickname()
|
||||||
|
} else {
|
||||||
onLoginSuccess()
|
onLoginSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 错误消息Snackbar
|
// 错误消息Snackbar
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
@@ -107,7 +107,7 @@ fun LoginScreen(
|
|||||||
|
|
||||||
// 标题
|
// 标题
|
||||||
Text(
|
Text(
|
||||||
text = if (needsNickname) "欢迎加入" else "欢迎",
|
text = "欢迎",
|
||||||
fontSize = 28.sp,
|
fontSize = 28.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
@@ -116,9 +116,7 @@ fun LoginScreen(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = if (needsNickname) {
|
text = if (!isPasswordMode) {
|
||||||
"设置昵称完成注册"
|
|
||||||
} else if (!isPasswordMode) {
|
|
||||||
"使用手机号验证码登录,首次登录将自动注册"
|
"使用手机号验证码登录,首次登录将自动注册"
|
||||||
} else {
|
} else {
|
||||||
"登录您的账号以继续"
|
"登录您的账号以继续"
|
||||||
@@ -177,25 +175,6 @@ fun LoginScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 如果需要输入昵称(首次登录),显示昵称输入框
|
|
||||||
if (needsNickname && !isPasswordMode) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = nickname,
|
|
||||||
onValueChange = {
|
|
||||||
if (it.length <= 50) {
|
|
||||||
nickname = it
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text("昵称") },
|
|
||||||
placeholder = { Text("请输入您的昵称(1-50个字符)") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
enabled = !isLoading
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据登录模式显示不同的输入框
|
// 根据登录模式显示不同的输入框
|
||||||
if (isPasswordMode) {
|
if (isPasswordMode) {
|
||||||
// 密码输入框
|
// 密码输入框
|
||||||
@@ -287,14 +266,6 @@ fun LoginScreen(
|
|||||||
modifier = Modifier.padding(bottom = 8.dp)
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (needsNickname && nickname.isNotEmpty() && nickname.trim().isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "昵称不能为空",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
@@ -371,14 +342,9 @@ fun LoginScreen(
|
|||||||
viewModel.login(trimmedPhone, password, agreedToTerms)
|
viewModel.login(trimmedPhone, password, agreedToTerms)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 验证码登录/注册
|
// 验证码登录/注册(无需昵称,首次登录后会跳转到昵称设置页面)
|
||||||
val trimmedNickname = if (needsNickname) nickname.trim() else null
|
|
||||||
if (trimmedPhone.length == 11 && smsCode.length == 6 && agreedToTerms) {
|
if (trimmedPhone.length == 11 && smsCode.length == 6 && agreedToTerms) {
|
||||||
// 如果需要昵称但未提供,不执行登录
|
viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms, null)
|
||||||
if (needsNickname && trimmedNickname.isNullOrBlank()) {
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms, trimmedNickname)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -389,7 +355,7 @@ fun LoginScreen(
|
|||||||
(if (isPasswordMode) {
|
(if (isPasswordMode) {
|
||||||
password.length >= 6
|
password.length >= 6
|
||||||
} else {
|
} else {
|
||||||
smsCode.length == 6 && (!needsNickname || nickname.trim().isNotEmpty())
|
smsCode.length == 6
|
||||||
}) &&
|
}) &&
|
||||||
agreedToTerms,
|
agreedToTerms,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -404,7 +370,7 @@ fun LoginScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = if (needsNickname) "注册并登录" else "登录",
|
text = "登录",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -485,22 +451,10 @@ fun LoginScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
showResultDialog = false
|
showResultDialog = false
|
||||||
viewModel.clearOperationResult()
|
viewModel.clearOperationResult()
|
||||||
if (operationResult?.success == true) {
|
|
||||||
// 成功时延迟跳转已在LaunchedEffect中处理
|
|
||||||
needsNickname = false
|
|
||||||
nickname = ""
|
|
||||||
} else {
|
|
||||||
// 失败时,如果是需要昵称的错误,不清除needsNickname状态
|
|
||||||
if (!(operationResult?.details?.contains("首次登录") == true ||
|
|
||||||
operationResult?.details?.contains("需要设置昵称") == true)) {
|
|
||||||
needsNickname = false
|
|
||||||
nickname = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (operationResult?.success == true) "确定" else if (needsNickname) "设置昵称" else "重试",
|
text = if (operationResult?.success == true) "确定" else "重试",
|
||||||
color = if (operationResult?.success == true) LightPurple else MaterialTheme.colorScheme.error
|
color = if (operationResult?.success == true) LightPurple else MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package com.huaga.life_echo.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.huaga.life_echo.ui.icons.AppIcons
|
||||||
|
import com.huaga.life_echo.ui.theme.LightPurple
|
||||||
|
import com.huaga.life_echo.ui.viewmodel.AuthViewModel
|
||||||
|
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NicknameSetupScreen(
|
||||||
|
onSetupComplete: () -> Unit,
|
||||||
|
viewModel: AuthViewModel = viewModel(
|
||||||
|
factory = ViewModelFactory(LocalContext.current)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
var nickname by remember { mutableStateOf("") }
|
||||||
|
var showResultDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||||
|
val operationResult by viewModel.operationResult.collectAsState()
|
||||||
|
|
||||||
|
// 错误消息Snackbar
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LaunchedEffect(errorMessage) {
|
||||||
|
errorMessage?.let { error ->
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = error,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示操作结果弹窗
|
||||||
|
LaunchedEffect(operationResult) {
|
||||||
|
operationResult?.let {
|
||||||
|
if (it.success) {
|
||||||
|
showResultDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
// 欢迎图标
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.PersonAdd,
|
||||||
|
contentDescription = "设置昵称",
|
||||||
|
modifier = Modifier.size(80.dp),
|
||||||
|
tint = LightPurple
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Text(
|
||||||
|
text = "欢迎加入岁月史书",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "请设置您的昵称,让我们更好地认识您",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// 昵称输入框
|
||||||
|
OutlinedTextField(
|
||||||
|
value = nickname,
|
||||||
|
onValueChange = {
|
||||||
|
if (it.length <= 50) {
|
||||||
|
nickname = it
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text("昵称") },
|
||||||
|
placeholder = { Text("请输入您的昵称(1-50个字符)") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
enabled = !isLoading
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 输入提示
|
||||||
|
if (nickname.isNotEmpty() && nickname.trim().isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "昵称不能为空格",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// 确认按钮
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val trimmedNickname = nickname.trim()
|
||||||
|
if (trimmedNickname.isNotEmpty()) {
|
||||||
|
viewModel.updateNickname(trimmedNickname) {
|
||||||
|
// 成功后会显示弹窗,弹窗关闭后跳转
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
enabled = !isLoading && nickname.trim().isNotEmpty(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = LightPurple,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "开始使用",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 提示文字
|
||||||
|
Text(
|
||||||
|
text = "昵称将用于您的回忆录展示",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作结果弹窗
|
||||||
|
if (showResultDialog && operationResult != null && operationResult?.success == true) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
showResultDialog = false
|
||||||
|
viewModel.clearOperationResult()
|
||||||
|
onSetupComplete()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.CheckCircle,
|
||||||
|
contentDescription = "成功",
|
||||||
|
tint = Color(0xFF4CAF50),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = operationResult?.message ?: "设置成功",
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = operationResult?.details ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showResultDialog = false
|
||||||
|
viewModel.clearOperationResult()
|
||||||
|
onSetupComplete()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "开始探索",
|
||||||
|
color = LightPurple
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ class AuthViewModel(private val context: Context) : ViewModel() {
|
|||||||
private val _currentUser = MutableStateFlow<UserResponse?>(null)
|
private val _currentUser = MutableStateFlow<UserResponse?>(null)
|
||||||
val currentUser: StateFlow<UserResponse?> = _currentUser.asStateFlow()
|
val currentUser: StateFlow<UserResponse?> = _currentUser.asStateFlow()
|
||||||
|
|
||||||
|
// 是否需要设置昵称(首次登录用户)
|
||||||
|
private val _needsNicknameSetup = MutableStateFlow(false)
|
||||||
|
val needsNicknameSetup: StateFlow<Boolean> = _needsNicknameSetup.asStateFlow()
|
||||||
|
|
||||||
// 操作状态:用于显示详细的操作结果
|
// 操作状态:用于显示详细的操作结果
|
||||||
data class OperationResult(
|
data class OperationResult(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
@@ -230,6 +234,8 @@ class AuthViewModel(private val context: Context) : ViewModel() {
|
|||||||
return result.fold(
|
return result.fold(
|
||||||
onSuccess = { userResponse ->
|
onSuccess = { userResponse ->
|
||||||
_currentUser.value = userResponse
|
_currentUser.value = userResponse
|
||||||
|
// 检查用户是否需要设置昵称(昵称为空表示首次登录)
|
||||||
|
_needsNicknameSetup.value = userResponse.nickname.isBlank()
|
||||||
// 更新TokenManager中的userId
|
// 更新TokenManager中的userId
|
||||||
TokenManager.saveTokens(
|
TokenManager.saveTokens(
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -629,4 +635,56 @@ class AuthViewModel(private val context: Context) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户昵称
|
||||||
|
*/
|
||||||
|
fun updateNickname(nickname: String, onSuccess: () -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_errorMessage.value = null
|
||||||
|
_successMessage.value = null
|
||||||
|
|
||||||
|
val accessToken = TokenManager.getAccessToken()
|
||||||
|
if (accessToken.isNullOrBlank()) {
|
||||||
|
_errorMessage.value = "未登录"
|
||||||
|
_isLoading.value = false
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = authService.updateNickname(accessToken, nickname)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { userResponse ->
|
||||||
|
_currentUser.value = userResponse
|
||||||
|
_needsNicknameSetup.value = false
|
||||||
|
_successMessage.value = "昵称设置成功"
|
||||||
|
_operationResult.value = OperationResult(
|
||||||
|
success = true,
|
||||||
|
message = "欢迎",
|
||||||
|
details = "欢迎加入岁月史书,${userResponse.nickname}!"
|
||||||
|
)
|
||||||
|
onSuccess()
|
||||||
|
},
|
||||||
|
onFailure = { exception ->
|
||||||
|
val errorMsg = exception.message ?: "设置昵称失败"
|
||||||
|
_errorMessage.value = errorMsg
|
||||||
|
_operationResult.value = OperationResult(
|
||||||
|
success = false,
|
||||||
|
message = "设置失败",
|
||||||
|
details = errorMsg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除需要设置昵称的状态
|
||||||
|
*/
|
||||||
|
fun clearNicknameSetupState() {
|
||||||
|
_needsNicknameSetup.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user