feat: 新增前端认证UI组件

- 新增auth/认证相关UI组件
This commit is contained in:
iammm0
2026-01-27 11:36:08 +08:00
parent 61b7c25440
commit 9989d63b4d
3 changed files with 353 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
package com.huaga.life_echo.ui.components.auth
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* 密码强度等级
*/
enum class PasswordStrength(val label: String, val color: Color) {
WEAK("", Color(0xFFEF5350)),
MEDIUM("", Color(0xFFFF9800)),
STRONG("", Color(0xFF66BB6A))
}
/**
* 计算密码强度
*/
fun calculatePasswordStrength(password: String): PasswordStrength? {
if (password.isEmpty()) return null
var score = 0
// 长度评分
when {
password.length >= 12 -> score += 3
password.length >= 8 -> score += 2
password.length >= 6 -> score += 1
}
// 包含小写字母
if (password.any { it.isLowerCase() }) score += 1
// 包含大写字母
if (password.any { it.isUpperCase() }) score += 1
// 包含数字
if (password.any { it.isDigit() }) score += 1
// 包含特殊字符
if (password.any { !it.isLetterOrDigit() }) score += 1
return when {
score >= 6 -> PasswordStrength.STRONG
score >= 4 -> PasswordStrength.MEDIUM
else -> PasswordStrength.WEAK
}
}
/**
* 密码强度指示器组件
*
* @param password 当前密码
*/
@Composable
fun PasswordStrengthIndicator(
password: String,
modifier: Modifier = Modifier
) {
val strength = remember(password) { calculatePasswordStrength(password) }
if (strength != null) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 强度条
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
repeat(3) { index ->
Box(
modifier = Modifier
.weight(1f)
.height(4.dp)
.background(
color = when {
index == 0 -> strength.color
index == 1 && strength != PasswordStrength.WEAK -> strength.color
index == 2 && strength == PasswordStrength.STRONG -> strength.color
else -> MaterialTheme.colorScheme.surfaceVariant
},
shape = RoundedCornerShape(2.dp)
)
)
}
}
// 强度文字
Text(
text = "强度:${strength.label}",
fontSize = 12.sp,
color = strength.color
)
}
}
}
/**
* 密码要求提示组件
*/
@Composable
fun PasswordRequirements(
password: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "密码要求:",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
PasswordRequirementItem(
text = "至少6个字符",
met = password.length >= 6
)
PasswordRequirementItem(
text = "建议包含大小写字母、数字和特殊字符",
met = password.any { it.isLowerCase() } &&
password.any { it.isUpperCase() } &&
password.any { it.isDigit() }
)
}
}
@Composable
private fun PasswordRequirementItem(
text: String,
met: Boolean
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (met) "" else "",
fontSize = 12.sp,
color = if (met) {
Color(0xFF66BB6A)
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
Text(
text = text,
fontSize = 12.sp,
color = if (met) {
Color(0xFF66BB6A)
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}

View File

@@ -0,0 +1,87 @@
package com.huaga.life_echo.ui.components.auth
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* 发送短信验证码按钮组件
*
* @param countdown 倒计时秒数0表示可以发送
* @param enabled 是否启用按钮
* @param onClick 点击回调
*/
@Composable
fun SendSmsButton(
countdown: Int,
enabled: Boolean = true,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val canSend = countdown == 0 && enabled
Button(
onClick = onClick,
enabled = canSend,
modifier = modifier.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (canSend) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.surfaceVariant
},
contentColor = if (canSend) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
) {
Text(
text = if (countdown > 0) {
"${countdown}秒后重发"
} else {
"发送验证码"
},
fontSize = 14.sp
)
}
}
/**
* 紧凑版发送短信验证码按钮(用于输入框右侧)
*/
@Composable
fun CompactSendSmsButton(
countdown: Int,
enabled: Boolean = true,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val canSend = countdown == 0 && enabled
TextButton(
onClick = onClick,
enabled = canSend,
modifier = modifier,
colors = ButtonDefaults.textButtonColors(
contentColor = if (canSend) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
) {
Text(
text = if (countdown > 0) {
"${countdown}s"
} else {
"获取验证码"
},
fontSize = 14.sp
)
}
}

View File

@@ -0,0 +1,96 @@
package com.huaga.life_echo.ui.components.auth
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* 短信验证码输入框组件
*
* @param code 当前输入的验证码
* @param onCodeChange 验证码变化回调
* @param codeLength 验证码长度默认6位
* @param enabled 是否启用输入
*/
@Composable
fun SmsCodeInput(
code: String,
onCodeChange: (String) -> Unit,
codeLength: Int = 6,
enabled: Boolean = true,
modifier: Modifier = Modifier
) {
BasicTextField(
value = code,
onValueChange = { newValue ->
// 只允许输入数字,且不超过指定长度
if (newValue.length <= codeLength && newValue.all { it.isDigit() }) {
onCodeChange(newValue)
}
},
enabled = enabled,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textStyle = TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
),
modifier = modifier,
decorationBox = { innerTextField ->
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
repeat(codeLength) { index ->
Box(
modifier = Modifier
.size(48.dp)
.border(
width = 2.dp,
color = when {
!enabled -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
index == code.length -> MaterialTheme.colorScheme.primary
index < code.length -> MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else -> MaterialTheme.colorScheme.outline
},
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = if (index < code.length) code[index].toString() else "",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
}
)
)
}
}
}
// 隐藏原始输入框
Box(modifier = Modifier.size(0.dp)) {
innerTextField()
}
}
)
}