feat: 新增前端认证UI组件
- 新增auth/认证相关UI组件
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user