feat: 新增前端认证相关屏幕

- 新增AccountManagementScreen账户管理屏幕
- 新增ResetPasswordScreen重置密码屏幕
This commit is contained in:
iammm0
2026-01-27 11:36:12 +08:00
parent 9989d63b4d
commit 42264f306e
2 changed files with 1091 additions and 0 deletions

View File

@@ -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("取消")
}
}
)
}

View File

@@ -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
)
}
}
}