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 PersonalInfo : Screen("personal_info")
|
||||
object Login : Screen("login")
|
||||
object NicknameSetup : Screen("nickname_setup")
|
||||
object ResetPassword : Screen("reset_password")
|
||||
object AccountManagement : Screen("account_management")
|
||||
object UpgradePlan : Screen("upgrade_plan")
|
||||
@@ -199,6 +200,13 @@ fun AppNavigation(
|
||||
popUpTo(Screen.Login.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onLoginSuccessNeedsNickname = {
|
||||
// 登录成功但需要设置昵称,导航到昵称设置页面
|
||||
navController.navigate(Screen.NicknameSetup.route) {
|
||||
// 清除登录页面,避免返回
|
||||
popUpTo(Screen.Login.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateToResetPassword = {
|
||||
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(
|
||||
route = Screen.ResetPassword.route,
|
||||
enterTransition = { slideInHorizontally() },
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
@@ -526,4 +527,40 @@ class AuthService {
|
||||
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(
|
||||
val message: String
|
||||
)
|
||||
|
||||
// 更新昵称请求
|
||||
@Serializable
|
||||
data class UpdateNicknameRequest(
|
||||
val nickname: String
|
||||
)
|
||||
|
||||
@@ -88,6 +88,7 @@ object AppIcons {
|
||||
val Phone = Icons.Default.Phone
|
||||
val ExitToApp = Icons.AutoMirrored.Filled.ExitToApp
|
||||
val DevicesOther = Icons.Default.DevicesOther
|
||||
val PersonAdd = Icons.Default.PersonAdd
|
||||
|
||||
// 错误处理图标
|
||||
val WifiOff = Icons.Default.WifiOff
|
||||
|
||||
@@ -65,6 +65,9 @@ fun ConversationListScreen(
|
||||
var isSelectionMode by remember { mutableStateOf(false) }
|
||||
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
|
||||
|
||||
// 是否正在自动创建对话
|
||||
var isAutoCreating by remember { mutableStateOf(false) }
|
||||
|
||||
// 刷新对话列表
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refreshConversations()
|
||||
@@ -72,16 +75,18 @@ fun ConversationListScreen(
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 处理新建对话
|
||||
val handleCreateConversation: () -> Unit = {
|
||||
scope.launch {
|
||||
// 当对话列表为空时,自动创建一个对话并进入
|
||||
LaunchedEffect(conversations, isLoading, isAutoCreating) {
|
||||
if (!isLoading && conversations.isEmpty() && !isAutoCreating) {
|
||||
isAutoCreating = true
|
||||
val result = viewModel.createConversation()
|
||||
result.fold(
|
||||
onSuccess = { conversationId ->
|
||||
onConversationClick(conversationId)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// 错误处理可以在这里添加
|
||||
// 创建失败,重置状态
|
||||
isAutoCreating = false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -175,14 +180,7 @@ fun ConversationListScreen(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 正常模式下的操作
|
||||
IconButton(onClick = handleCreateConversation) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Add,
|
||||
contentDescription = "新建对话",
|
||||
tint = LightPurple
|
||||
)
|
||||
}
|
||||
// 正常模式下不显示任何操作按钮(禁止创建多个对话)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -210,12 +208,16 @@ fun ConversationListScreen(
|
||||
)
|
||||
}
|
||||
conversations.isEmpty() -> {
|
||||
// 空状态 - 提示用户创建新对话
|
||||
EmptyStateView(
|
||||
title = "还没有对话",
|
||||
message = "点击右上角「+」按钮开始您的回忆录之旅",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
// 空状态 - 正在自动创建对话
|
||||
if (isAutoCreating) {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
EmptyStateView(
|
||||
title = "正在初始化",
|
||||
message = "正在为您准备回忆录对话...",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
@@ -243,16 +245,20 @@ fun ConversationListScreen(
|
||||
onConversationClick(conversation.id)
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
scope.launch {
|
||||
viewModel.deleteConversation(conversation.id)
|
||||
// 只有一个对话时禁止删除
|
||||
onDelete = if (conversations.size > 1) {
|
||||
{
|
||||
scope.launch {
|
||||
viewModel.deleteConversation(conversation.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
} else null,
|
||||
isSelected = selectedIds.contains(conversation.id),
|
||||
isSelectionMode = isSelectionMode,
|
||||
onLongClick = {
|
||||
handleLongClick(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
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onLoginSuccessNeedsNickname: () -> Unit = {},
|
||||
onNavigateToResetPassword: (() -> Unit)? = null,
|
||||
onNavigateToTerms: () -> Unit = {},
|
||||
onNavigateToPrivacy: () -> Unit = {},
|
||||
@@ -44,8 +45,6 @@ fun LoginScreen(
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var smsCode by remember { mutableStateOf("") }
|
||||
var nickname by remember { mutableStateOf("") } // 首次登录时的昵称
|
||||
var needsNickname by remember { mutableStateOf(false) } // 是否需要输入昵称
|
||||
var agreedToTerms by remember { mutableStateOf(false) }
|
||||
var showResultDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -55,26 +54,27 @@ fun LoginScreen(
|
||||
val operationResult by viewModel.operationResult.collectAsState()
|
||||
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
|
||||
val smsCountdown by viewModel.smsCountdown.collectAsState()
|
||||
val needsNicknameSetup by viewModel.needsNicknameSetup.collectAsState()
|
||||
|
||||
// 显示操作结果弹窗,并检查是否需要输入昵称
|
||||
// 显示操作结果弹窗
|
||||
LaunchedEffect(operationResult) {
|
||||
operationResult?.let {
|
||||
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) {
|
||||
kotlinx.coroutines.delay(1500) // 显示1.5秒成功提示后跳转
|
||||
viewModel.clearSuccess()
|
||||
viewModel.clearOperationResult()
|
||||
onLoginSuccess()
|
||||
// 根据是否需要设置昵称决定导航目标
|
||||
if (needsNicknameSetup) {
|
||||
onLoginSuccessNeedsNickname()
|
||||
} else {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fun LoginScreen(
|
||||
|
||||
// 标题
|
||||
Text(
|
||||
text = if (needsNickname) "欢迎加入" else "欢迎",
|
||||
text = "欢迎",
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
@@ -116,9 +116,7 @@ fun LoginScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = if (needsNickname) {
|
||||
"设置昵称完成注册"
|
||||
} else if (!isPasswordMode) {
|
||||
text = if (!isPasswordMode) {
|
||||
"使用手机号验证码登录,首次登录将自动注册"
|
||||
} else {
|
||||
"登录您的账号以继续"
|
||||
@@ -177,25 +175,6 @@ fun LoginScreen(
|
||||
|
||||
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) {
|
||||
// 密码输入框
|
||||
@@ -287,14 +266,6 @@ fun LoginScreen(
|
||||
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))
|
||||
|
||||
@@ -371,14 +342,9 @@ fun LoginScreen(
|
||||
viewModel.login(trimmedPhone, password, agreedToTerms)
|
||||
}
|
||||
} else {
|
||||
// 验证码登录/注册
|
||||
val trimmedNickname = if (needsNickname) nickname.trim() else null
|
||||
// 验证码登录/注册(无需昵称,首次登录后会跳转到昵称设置页面)
|
||||
if (trimmedPhone.length == 11 && smsCode.length == 6 && agreedToTerms) {
|
||||
// 如果需要昵称但未提供,不执行登录
|
||||
if (needsNickname && trimmedNickname.isNullOrBlank()) {
|
||||
return@Button
|
||||
}
|
||||
viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms, trimmedNickname)
|
||||
viewModel.loginWithSms(trimmedPhone, smsCode, agreedToTerms, null)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -389,7 +355,7 @@ fun LoginScreen(
|
||||
(if (isPasswordMode) {
|
||||
password.length >= 6
|
||||
} else {
|
||||
smsCode.length == 6 && (!needsNickname || nickname.trim().isNotEmpty())
|
||||
smsCode.length == 6
|
||||
}) &&
|
||||
agreedToTerms,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -404,7 +370,7 @@ fun LoginScreen(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = if (needsNickname) "注册并登录" else "登录",
|
||||
text = "登录",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -485,22 +451,10 @@ fun LoginScreen(
|
||||
onClick = {
|
||||
showResultDialog = false
|
||||
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 = if (operationResult?.success == true) "确定" else if (needsNickname) "设置昵称" else "重试",
|
||||
text = if (operationResult?.success == true) "确定" else "重试",
|
||||
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)
|
||||
val currentUser: StateFlow<UserResponse?> = _currentUser.asStateFlow()
|
||||
|
||||
// 是否需要设置昵称(首次登录用户)
|
||||
private val _needsNicknameSetup = MutableStateFlow(false)
|
||||
val needsNicknameSetup: StateFlow<Boolean> = _needsNicknameSetup.asStateFlow()
|
||||
|
||||
// 操作状态:用于显示详细的操作结果
|
||||
data class OperationResult(
|
||||
val success: Boolean,
|
||||
@@ -230,6 +234,8 @@ class AuthViewModel(private val context: Context) : ViewModel() {
|
||||
return result.fold(
|
||||
onSuccess = { userResponse ->
|
||||
_currentUser.value = userResponse
|
||||
// 检查用户是否需要设置昵称(昵称为空表示首次登录)
|
||||
_needsNicknameSetup.value = userResponse.nickname.isBlank()
|
||||
// 更新TokenManager中的userId
|
||||
TokenManager.saveTokens(
|
||||
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