From 42264f306e17145cfd01b6a1ac44a2a97c1a1f1d Mon Sep 17 00:00:00 2001 From: iammm0 Date: Tue, 27 Jan 2026 11:36:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=9B=B8=E5=85=B3=E5=B1=8F=E5=B9=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增AccountManagementScreen账户管理屏幕 - 新增ResetPasswordScreen重置密码屏幕 --- .../ui/screens/AccountManagementScreen.kt | 701 ++++++++++++++++++ .../ui/screens/ResetPasswordScreen.kt | 390 ++++++++++ 2 files changed, 1091 insertions(+) create mode 100644 app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AccountManagementScreen.kt create mode 100644 app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AccountManagementScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AccountManagementScreen.kt new file mode 100644 index 0000000..0105e6d --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/AccountManagementScreen.kt @@ -0,0 +1,701 @@ +package com.huaga.life_echo.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.huaga.life_echo.ui.components.auth.CompactSendSmsButton +import com.huaga.life_echo.ui.components.auth.PasswordStrengthIndicator +import com.huaga.life_echo.ui.components.auth.SmsCodeInput +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 + +/** + * 账户管理页面 + * + * 功能模块: + * 1. 修改密码 + * 2. 修改手机号 + * 3. 登出管理(当前设备/所有设备) + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountManagementScreen( + onNavigateBack: () -> Unit, + onLogoutSuccess: () -> Unit, + viewModel: AuthViewModel = viewModel( + factory = ViewModelFactory(LocalContext.current) + ) +) { + var showChangePasswordDialog by remember { mutableStateOf(false) } + var showChangePhoneDialog by remember { mutableStateOf(false) } + var showLogoutDialog by remember { mutableStateOf(false) } + var showLogoutAllDialog by remember { mutableStateOf(false) } + + val isLoading by viewModel.isLoading.collectAsState() + val errorMessage by viewModel.errorMessage.collectAsState() + val currentUser by viewModel.currentUser.collectAsState() + + // 错误消息Snackbar + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(errorMessage) { + errorMessage?.let { error -> + snackbarHostState.showSnackbar( + message = error, + duration = SnackbarDuration.Long + ) + viewModel.clearError() + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("账户管理") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "返回" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) + }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + // 账户信息卡片 + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "账户信息", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(12.dp)) + + InfoRow(label = "昵称", value = currentUser?.nickname ?: "") + Spacer(modifier = Modifier.height(8.dp)) + InfoRow(label = "手机号", value = currentUser?.phone ?: "") + if (!currentUser?.email.isNullOrBlank()) { + Spacer(modifier = Modifier.height(8.dp)) + InfoRow(label = "邮箱", value = currentUser?.email ?: "") + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 安全设置 + Text( + text = "安全设置", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(vertical = 8.dp) + ) { + AccountManagementItem( + icon = AppIcons.Lock, + title = "修改密码", + subtitle = "定期修改密码保护账户安全", + onClick = { showChangePasswordDialog = true } + ) + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + + AccountManagementItem( + icon = AppIcons.Phone, + title = "修改手机号", + subtitle = "当前手机号:${currentUser?.phone ?: ""}", + onClick = { showChangePhoneDialog = true } + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 登出管理 + Text( + text = "登出管理", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column( + modifier = Modifier.padding(vertical = 8.dp) + ) { + AccountManagementItem( + icon = AppIcons.ExitToApp, + title = "登出当前设备", + subtitle = "仅登出当前设备", + onClick = { showLogoutDialog = true } + ) + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + + AccountManagementItem( + icon = AppIcons.DevicesOther, + title = "登出所有设备", + subtitle = "登出所有已登录的设备", + iconTint = MaterialTheme.colorScheme.error, + onClick = { showLogoutAllDialog = true } + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } + + // 修改密码对话框 + if (showChangePasswordDialog) { + ChangePasswordDialog( + viewModel = viewModel, + onDismiss = { showChangePasswordDialog = false }, + onSuccess = { showChangePasswordDialog = false } + ) + } + + // 修改手机号对话框 + if (showChangePhoneDialog) { + ChangePhoneDialog( + viewModel = viewModel, + onDismiss = { showChangePhoneDialog = false }, + onSuccess = { showChangePhoneDialog = false } + ) + } + + // 登出当前设备确认对话框 + if (showLogoutDialog) { + AlertDialog( + onDismissRequest = { showLogoutDialog = false }, + title = { Text("确认登出") }, + text = { Text("确定要登出当前设备吗?") }, + confirmButton = { + TextButton( + onClick = { + viewModel.logout() + showLogoutDialog = false + onLogoutSuccess() + } + ) { + Text("确认", color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { showLogoutDialog = false }) { + Text("取消") + } + } + ) + } + + // 登出所有设备确认对话框 + if (showLogoutAllDialog) { + AlertDialog( + onDismissRequest = { showLogoutAllDialog = false }, + title = { Text("确认登出所有设备") }, + text = { + Text("确定要登出所有设备吗?这将使您在所有设备上的登录状态失效,需要重新登录。") + }, + confirmButton = { + TextButton( + onClick = { + viewModel.logoutAll { + showLogoutAllDialog = false + onLogoutSuccess() + } + } + ) { + Text("确认登出", color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { showLogoutAllDialog = false }) { + Text("取消") + } + } + ) + } +} + +/** + * 账户管理菜单项 + */ +@Composable +private fun AccountManagementItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + title: String, + subtitle: String? = null, + iconTint: Color = LightPurple, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 图标 + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) + .background(iconTint.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = title, + tint = iconTint, + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // 文本 + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface + ) + if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = subtitle, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // 箭头 + Icon( + imageVector = AppIcons.ChevronRight, + contentDescription = "进入", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } +} + +/** + * 信息行 + */ +@Composable +private fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = value, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +/** + * 修改密码对话框 + */ +@Composable +private fun ChangePasswordDialog( + viewModel: AuthViewModel, + onDismiss: () -> Unit, + onSuccess: () -> Unit +) { + var oldPassword by remember { mutableStateOf("") } + var newPassword by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + var oldPasswordVisible by remember { mutableStateOf(false) } + var newPasswordVisible by remember { mutableStateOf(false) } + var confirmPasswordVisible by remember { mutableStateOf(false) } + + val isLoading by viewModel.isLoading.collectAsState() + val operationResult by viewModel.operationResult.collectAsState() + + // 表单验证 + val isOldPasswordValid = oldPassword.length >= 6 + val isNewPasswordValid = newPassword.length >= 6 + val isConfirmPasswordValid = confirmPassword == newPassword && confirmPassword.isNotEmpty() + val canSubmit = isOldPasswordValid && isNewPasswordValid && isConfirmPasswordValid && !isLoading + + // 监听操作结果 + LaunchedEffect(operationResult) { + if (operationResult?.success == true) { + kotlinx.coroutines.delay(1500) + viewModel.clearOperationResult() + onSuccess() + } + } + + AlertDialog( + onDismissRequest = { if (!isLoading) onDismiss() }, + title = { Text("修改密码") }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // 旧密码 + OutlinedTextField( + value = oldPassword, + onValueChange = { oldPassword = it }, + label = { Text("当前密码") }, + placeholder = { Text("请输入当前密码") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (oldPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { oldPasswordVisible = !oldPasswordVisible }) { + Icon( + imageVector = if (oldPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (oldPasswordVisible) "隐藏密码" else "显示密码" + ) + } + }, + enabled = !isLoading + ) + + // 新密码 + OutlinedTextField( + value = newPassword, + onValueChange = { newPassword = it }, + label = { Text("新密码") }, + placeholder = { Text("请输入新密码(至少6位)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) { + Icon( + imageVector = if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (newPasswordVisible) "隐藏密码" else "显示密码" + ) + } + }, + enabled = !isLoading, + isError = newPassword.isNotEmpty() && !isNewPasswordValid + ) + + // 密码强度指示器 + if (newPassword.isNotEmpty()) { + PasswordStrengthIndicator(password = newPassword) + } + + // 确认新密码 + OutlinedTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + label = { Text("确认新密码") }, + placeholder = { Text("请再次输入新密码") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) { + Icon( + imageVector = if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (confirmPasswordVisible) "隐藏密码" else "显示密码" + ) + } + }, + enabled = !isLoading, + isError = confirmPassword.isNotEmpty() && !isConfirmPasswordValid + ) + + if (confirmPassword.isNotEmpty() && !isConfirmPasswordValid) { + Text( + text = "两次输入的密码不一致", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error + ) + } + + // 显示操作结果 + operationResult?.let { result -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = if (result.success) AppIcons.CheckCircle else AppIcons.Error, + contentDescription = null, + tint = if (result.success) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + Text( + text = result.message, + fontSize = 14.sp, + color = if (result.success) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error + ) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + viewModel.changePassword(oldPassword, newPassword) { + // 成功回调已通过operationResult处理 + } + }, + enabled = canSubmit + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp + ) + } else { + Text("确认修改", color = LightPurple) + } + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + enabled = !isLoading + ) { + Text("取消") + } + } + ) +} + +/** + * 修改手机号对话框 + */ +@Composable +private fun ChangePhoneDialog( + viewModel: AuthViewModel, + onDismiss: () -> Unit, + onSuccess: () -> Unit +) { + var newPhone by remember { mutableStateOf("") } + var code by remember { mutableStateOf("") } + + val isLoading by viewModel.isLoading.collectAsState() + val smsCountdown by viewModel.smsCountdown.collectAsState() + val operationResult by viewModel.operationResult.collectAsState() + + // 表单验证 + val isPhoneValid = newPhone.length == 11 + val isCodeValid = code.length == 6 + val canSubmit = isPhoneValid && isCodeValid && !isLoading + + // 监听操作结果 + LaunchedEffect(operationResult) { + if (operationResult?.success == true && operationResult?.message?.contains("手机号修改成功") == true) { + kotlinx.coroutines.delay(1500) + viewModel.clearOperationResult() + onSuccess() + } + } + + AlertDialog( + onDismissRequest = { if (!isLoading) onDismiss() }, + title = { Text("修改手机号") }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // 新手机号输入(带发送验证码按钮) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = newPhone, + onValueChange = { + if (it.length <= 11) { + newPhone = it.filter { char -> char.isDigit() } + } + }, + label = { Text("新手机号") }, + placeholder = { Text("请输入新手机号") }, + modifier = Modifier.weight(1f), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + enabled = !isLoading, + isError = newPhone.isNotEmpty() && !isPhoneValid + ) + } + + // 发送验证码按钮 + Button( + onClick = { + viewModel.sendVerificationCode(newPhone, "change_phone") + }, + enabled = isPhoneValid && smsCountdown == 0 && !isLoading, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = if (smsCountdown > 0) { + "${smsCountdown}秒后重发" + } else { + "发送验证码" + } + ) + } + + // 验证码输入 + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "验证码", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + SmsCodeInput( + code = code, + onCodeChange = { code = it }, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth() + ) + } + + // 显示操作结果 + operationResult?.let { result -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = if (result.success) AppIcons.CheckCircle else AppIcons.Error, + contentDescription = null, + tint = if (result.success) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + Text( + text = result.message, + fontSize = 14.sp, + color = if (result.success) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error + ) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + viewModel.changePhone(newPhone, code) { + // 成功回调已通过operationResult处理 + } + }, + enabled = canSubmit + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp + ) + } else { + Text("确认修改", color = LightPurple) + } + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + enabled = !isLoading + ) { + Text("取消") + } + } + ) +} diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt new file mode 100644 index 0000000..66e22e3 --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/ResetPasswordScreen.kt @@ -0,0 +1,390 @@ +package com.huaga.life_echo.ui.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +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.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.huaga.life_echo.ui.components.auth.CompactSendSmsButton +import com.huaga.life_echo.ui.components.auth.PasswordStrengthIndicator +import com.huaga.life_echo.ui.components.auth.SmsCodeInput +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 ResetPasswordScreen( + onResetSuccess: () -> Unit, + onBackToLogin: () -> Unit, + viewModel: AuthViewModel = viewModel( + factory = ViewModelFactory(LocalContext.current) + ) +) { + var phone by remember { mutableStateOf("") } + var code by remember { mutableStateOf("") } + var newPassword by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + var newPasswordVisible by remember { mutableStateOf(false) } + var confirmPasswordVisible by remember { mutableStateOf(false) } + var showSuccessDialog by remember { mutableStateOf(false) } + + val isLoading by viewModel.isLoading.collectAsState() + val errorMessage by viewModel.errorMessage.collectAsState() + val successMessage by viewModel.successMessage.collectAsState() + val smsCountdown by viewModel.smsCountdown.collectAsState() + val operationResult by viewModel.operationResult.collectAsState() + + // 表单验证 + val isPhoneValid = phone.length == 11 + val isCodeValid = code.length == 6 + val isNewPasswordValid = newPassword.length >= 6 + val isConfirmPasswordValid = confirmPassword == newPassword && confirmPassword.isNotEmpty() + val canSubmit = isPhoneValid && isCodeValid && isNewPasswordValid && isConfirmPasswordValid && !isLoading + + // 错误消息Snackbar + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(errorMessage) { + errorMessage?.let { error -> + snackbarHostState.showSnackbar( + message = error, + duration = SnackbarDuration.Long + ) + viewModel.clearError() + } + } + + // 监听成功消息 + LaunchedEffect(successMessage) { + successMessage?.let { + showSuccessDialog = true + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("重置密码") }, + navigationIcon = { + IconButton(onClick = onBackToLogin) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "返回" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) + }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.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)) + + // 手机号输入框(带发送验证码按钮) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = phone, + onValueChange = { + if (it.length <= 11) { + phone = it.filter { char -> char.isDigit() } + } + }, + label = { Text("手机号") }, + placeholder = { Text("请输入11位手机号") }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = RoundedCornerShape(16.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + enabled = !isLoading, + isError = phone.isNotEmpty() && !isPhoneValid + ) + + CompactSendSmsButton( + countdown = smsCountdown, + enabled = isPhoneValid && !isLoading, + onClick = { + viewModel.sendVerificationCode(phone, "reset_password") + }, + modifier = Modifier.width(100.dp) + ) + } + + if (phone.isNotEmpty() && !isPhoneValid) { + Text( + text = "请输入11位手机号", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 验证码输入 + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "验证码", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + + SmsCodeInput( + code = code, + onCodeChange = { code = it }, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth() + ) + + if (code.isNotEmpty() && !isCodeValid) { + Text( + text = "请输入6位验证码", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 新密码输入框 + OutlinedTextField( + value = newPassword, + onValueChange = { newPassword = it }, + label = { Text("新密码") }, + placeholder = { Text("请输入新密码(至少6位)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(16.dp), + visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { newPasswordVisible = !newPasswordVisible }) { + Icon( + imageVector = if (newPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (newPasswordVisible) "隐藏密码" else "显示密码" + ) + } + }, + enabled = !isLoading, + isError = newPassword.isNotEmpty() && !isNewPasswordValid + ) + + if (newPassword.isNotEmpty() && !isNewPasswordValid) { + Text( + text = "密码至少需要6位", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 4.dp) + ) + } + + // 密码强度指示器 + if (newPassword.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + PasswordStrengthIndicator( + password = newPassword, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 确认密码输入框 + OutlinedTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + label = { Text("确认新密码") }, + placeholder = { Text("请再次输入新密码") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(16.dp), + visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) { + Icon( + imageVector = if (confirmPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = if (confirmPasswordVisible) "隐藏密码" else "显示密码" + ) + } + }, + enabled = !isLoading, + isError = confirmPassword.isNotEmpty() && !isConfirmPasswordValid + ) + + if (confirmPassword.isNotEmpty() && !isConfirmPasswordValid) { + Text( + text = "两次输入的密码不一致", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // 重置密码按钮 + Button( + onClick = { + viewModel.resetPassword(phone, code, newPassword) { + // 成功回调已通过successMessage处理 + } + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + enabled = canSubmit, + 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)) + + // 返回登录 + TextButton(onClick = onBackToLogin) { + Text( + text = "返回登录", + color = LightPurple, + fontSize = 14.sp + ) + } + } + + // 成功对话框 + if (showSuccessDialog && operationResult?.success == true) { + AlertDialog( + onDismissRequest = { }, + 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 = "密码重置成功", + fontWeight = FontWeight.Bold + ) + } + }, + text = { + Text( + text = "您的密码已成功重置,请使用新密码登录", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + confirmButton = { + TextButton( + onClick = { + showSuccessDialog = false + viewModel.clearSuccess() + viewModel.clearOperationResult() + onResetSuccess() + } + ) { + Text("去登录", color = LightPurple) + } + }, + containerColor = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.medium + ) + } + } +}