feat: 新增前端认证相关屏幕
- 新增AccountManagementScreen账户管理屏幕 - 新增ResetPasswordScreen重置密码屏幕
This commit is contained in:
@@ -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("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user