新增验证码
This commit is contained in:
@@ -3,6 +3,7 @@ package com.aiosman.riderpro.data
|
|||||||
import com.aiosman.riderpro.AppState
|
import com.aiosman.riderpro.AppState
|
||||||
import com.aiosman.riderpro.data.api.ApiClient
|
import com.aiosman.riderpro.data.api.ApiClient
|
||||||
import com.aiosman.riderpro.data.api.AppConfig
|
import com.aiosman.riderpro.data.api.AppConfig
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaInfo
|
||||||
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
|
import com.aiosman.riderpro.data.api.ChangePasswordRequestBody
|
||||||
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
|
import com.aiosman.riderpro.data.api.GoogleRegisterRequestBody
|
||||||
import com.aiosman.riderpro.data.api.LoginUserRequestBody
|
import com.aiosman.riderpro.data.api.LoginUserRequestBody
|
||||||
@@ -266,8 +267,13 @@ interface AccountService {
|
|||||||
* 使用用户名密码登录
|
* 使用用户名密码登录
|
||||||
* @param loginName 用户名
|
* @param loginName 用户名
|
||||||
* @param password 密码
|
* @param password 密码
|
||||||
|
* @param captchaInfo 验证码信息
|
||||||
*/
|
*/
|
||||||
suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth
|
suspend fun loginUserWithPassword(
|
||||||
|
loginName: String,
|
||||||
|
password: String,
|
||||||
|
captchaInfo: CaptchaInfo? = null
|
||||||
|
): UserAuth
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用google登录
|
* 使用google登录
|
||||||
@@ -383,8 +389,16 @@ class AccountServiceImpl : AccountService {
|
|||||||
return UserAuth(body.id)
|
return UserAuth(body.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loginUserWithPassword(loginName: String, password: String): UserAuth {
|
override suspend fun loginUserWithPassword(
|
||||||
val resp = ApiClient.api.login(LoginUserRequestBody(loginName, password))
|
loginName: String,
|
||||||
|
password: String,
|
||||||
|
captchaInfo: CaptchaInfo?
|
||||||
|
): UserAuth {
|
||||||
|
val resp = ApiClient.api.login(LoginUserRequestBody(
|
||||||
|
username = loginName,
|
||||||
|
password = password,
|
||||||
|
captcha = captchaInfo,
|
||||||
|
))
|
||||||
if (!resp.isSuccessful) {
|
if (!resp.isSuccessful) {
|
||||||
parseErrorResponse(resp.errorBody())?.let {
|
parseErrorResponse(resp.errorBody())?.let {
|
||||||
throw it.toServiceException()
|
throw it.toServiceException()
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.aiosman.riderpro.data
|
||||||
|
|
||||||
|
import com.aiosman.riderpro.data.api.ApiClient
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaRequestBody
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaResponseBody
|
||||||
|
import com.aiosman.riderpro.data.api.CheckLoginCaptchaRequestBody
|
||||||
|
import com.aiosman.riderpro.data.api.GenerateLoginCaptchaRequestBody
|
||||||
|
|
||||||
|
|
||||||
|
interface CaptchaService {
|
||||||
|
suspend fun generateCaptcha(source: String): CaptchaResponseBody
|
||||||
|
suspend fun checkLoginCaptcha(username: String): Boolean
|
||||||
|
suspend fun generateLoginCaptcha(username: String): CaptchaResponseBody
|
||||||
|
}
|
||||||
|
|
||||||
|
class CaptchaServiceImpl : CaptchaService {
|
||||||
|
override suspend fun generateCaptcha(source: String): CaptchaResponseBody {
|
||||||
|
val resp = ApiClient.api.generateCaptcha(
|
||||||
|
CaptchaRequestBody(source)
|
||||||
|
)
|
||||||
|
val data = resp.body() ?: throw Exception("Failed to generate captcha")
|
||||||
|
return data.data.copy(
|
||||||
|
masterBase64 = data.data.masterBase64.replace("data:image/jpeg;base64,", ""),
|
||||||
|
thumbBase64 = data.data.thumbBase64.replace("data:image/png;base64,", "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun checkLoginCaptcha(username: String): Boolean {
|
||||||
|
val resp = ApiClient.api.checkLoginCaptcha(
|
||||||
|
CheckLoginCaptchaRequestBody(username)
|
||||||
|
)
|
||||||
|
return resp.body()?.data ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun generateLoginCaptcha(username: String): CaptchaResponseBody {
|
||||||
|
val resp = ApiClient.api.generateLoginCaptcha(
|
||||||
|
GenerateLoginCaptchaRequestBody(username)
|
||||||
|
)
|
||||||
|
val data = resp.body() ?: throw Exception("Failed to generate captcha")
|
||||||
|
return data.data.copy(
|
||||||
|
masterBase64 = data.data.masterBase64.replace("data:image/jpeg;base64,", ""),
|
||||||
|
thumbBase64 = data.data.thumbBase64.replace("data:image/png;base64,", "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ data class LoginUserRequestBody(
|
|||||||
val password: String? = null,
|
val password: String? = null,
|
||||||
@SerializedName("googleId")
|
@SerializedName("googleId")
|
||||||
val googleId: String? = null,
|
val googleId: String? = null,
|
||||||
|
@SerializedName("captcha")
|
||||||
|
val captcha: CaptchaInfo? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GoogleRegisterRequestBody(
|
data class GoogleRegisterRequestBody(
|
||||||
@@ -128,6 +130,69 @@ data class DictItem(
|
|||||||
val desc: String,
|
val desc: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CaptchaRequestBody(
|
||||||
|
@SerializedName("source")
|
||||||
|
val source: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CaptchaResponseBody(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("thumb_base64")
|
||||||
|
val thumbBase64: String,
|
||||||
|
@SerializedName("master_base64")
|
||||||
|
val masterBase64: String,
|
||||||
|
@SerializedName("count")
|
||||||
|
val count: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CheckLoginCaptchaRequestBody(
|
||||||
|
@SerializedName("username")
|
||||||
|
val username: String,
|
||||||
|
)
|
||||||
|
data class GenerateLoginCaptchaRequestBody(
|
||||||
|
@SerializedName("username")
|
||||||
|
val username: String,
|
||||||
|
)
|
||||||
|
//{
|
||||||
|
// "id":48,
|
||||||
|
// "dot": [
|
||||||
|
// {
|
||||||
|
// "index": 0,
|
||||||
|
// "x": 76,
|
||||||
|
// "y": 165
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "index": 1,
|
||||||
|
// "x": 144,
|
||||||
|
// "y": 21
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "index": 2,
|
||||||
|
// "x": 220,
|
||||||
|
// "y": 42
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "index": 3,
|
||||||
|
// "x": 10,
|
||||||
|
// "y": 10
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
//}
|
||||||
|
data class DotPosition(
|
||||||
|
@SerializedName("index")
|
||||||
|
val index: Int,
|
||||||
|
@SerializedName("x")
|
||||||
|
val x: Int,
|
||||||
|
@SerializedName("y")
|
||||||
|
val y: Int,
|
||||||
|
)
|
||||||
|
data class CaptchaInfo(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("dot")
|
||||||
|
val dot: List<DotPosition>
|
||||||
|
)
|
||||||
interface RiderProAPI {
|
interface RiderProAPI {
|
||||||
@POST("register")
|
@POST("register")
|
||||||
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
|
||||||
@@ -336,4 +401,20 @@ interface RiderProAPI {
|
|||||||
suspend fun getDict(
|
suspend fun getDict(
|
||||||
@Query("key") key: String
|
@Query("key") key: String
|
||||||
): Response<DataContainer<DictItem>>
|
): Response<DataContainer<DictItem>>
|
||||||
|
|
||||||
|
@POST("captcha/generate")
|
||||||
|
suspend fun generateCaptcha(
|
||||||
|
@Body body: CaptchaRequestBody
|
||||||
|
): Response<DataContainer<CaptchaResponseBody>>
|
||||||
|
|
||||||
|
@POST("login/needCaptcha")
|
||||||
|
suspend fun checkLoginCaptcha(
|
||||||
|
@Body body: CheckLoginCaptchaRequestBody
|
||||||
|
): Response<DataContainer<Boolean>>
|
||||||
|
|
||||||
|
@POST("captcha/login/generate")
|
||||||
|
suspend fun generateLoginCaptcha(
|
||||||
|
@Body body: GenerateLoginCaptchaRequestBody
|
||||||
|
): Response<DataContainer<CaptchaResponseBody>>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ActionButton(
|
fun ActionButton(
|
||||||
modifier: Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
color: Color = Color.Black,
|
color: Color = Color.Black,
|
||||||
@DrawableRes backgroundImage: Int? = null,
|
@DrawableRes backgroundImage: Int? = null,
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.aiosman.riderpro.ui.composables
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material.AlertDialog
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaResponseBody
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ClickCaptchaView(
|
||||||
|
captchaData: CaptchaResponseBody,
|
||||||
|
onPositionClicked: (Offset) -> Unit
|
||||||
|
) {
|
||||||
|
var clickPositions by remember { mutableStateOf(listOf<Offset>()) }
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val imageBitmap = remember(captchaData.masterBase64) {
|
||||||
|
val decodedString = Base64.decode(captchaData.masterBase64, Base64.DEFAULT)
|
||||||
|
val inputStream = ByteArrayInputStream(decodedString)
|
||||||
|
BitmapFactory.decodeStream(inputStream).asImageBitmap()
|
||||||
|
}
|
||||||
|
val thumbnailBitmap = remember(captchaData.thumbBase64) {
|
||||||
|
val decodedString = Base64.decode(captchaData.thumbBase64, Base64.DEFAULT)
|
||||||
|
val inputStream = ByteArrayInputStream(decodedString)
|
||||||
|
BitmapFactory.decodeStream(inputStream).asImageBitmap()
|
||||||
|
}
|
||||||
|
var boxWidth by remember { mutableStateOf(0) }
|
||||||
|
var boxHeightInDp by remember { mutableStateOf(0.dp) }
|
||||||
|
var scale by remember { mutableStateOf(1f) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(stringResource(R.string.captcha_hint))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Image(
|
||||||
|
bitmap = thumbnailBitmap,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned {
|
||||||
|
boxWidth = it.size.width
|
||||||
|
scale = imageBitmap.width.toFloat() / boxWidth
|
||||||
|
boxHeightInDp = with(density) { (imageBitmap.height.toFloat() / scale).toDp() }
|
||||||
|
}
|
||||||
|
.background(Color.Gray)
|
||||||
|
) {
|
||||||
|
if (boxWidth != 0 && boxHeightInDp != 0.dp) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(boxHeightInDp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = imageBitmap,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInteropFilter { event ->
|
||||||
|
if (event.action == android.view.MotionEvent.ACTION_DOWN) {
|
||||||
|
val newPosition = Offset(event.x, event.y)
|
||||||
|
clickPositions = clickPositions + newPosition
|
||||||
|
// 计算出点击的位置在图片上的坐标
|
||||||
|
val imagePosition = Offset(
|
||||||
|
newPosition.x * scale,
|
||||||
|
newPosition.y * scale
|
||||||
|
)
|
||||||
|
onPositionClicked(imagePosition)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw markers at click positions
|
||||||
|
clickPositions.forEachIndexed { index, position ->
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(
|
||||||
|
color = Color(0xaada3832).copy(),
|
||||||
|
radius = 40f,
|
||||||
|
center = position
|
||||||
|
)
|
||||||
|
drawContext.canvas.nativeCanvas.apply {
|
||||||
|
drawText(
|
||||||
|
(index + 1).toString(),
|
||||||
|
position.x,
|
||||||
|
position.y + 15f, // Adjusting the y position to center the text
|
||||||
|
android.graphics.Paint().apply {
|
||||||
|
color = android.graphics.Color.WHITE
|
||||||
|
textSize = 50f
|
||||||
|
textAlign = android.graphics.Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickCaptchaDialog(
|
||||||
|
captchaData: CaptchaResponseBody,
|
||||||
|
onLoadCaptcha: () -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onPositionClicked: (Offset) -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text("Captcha")
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
ClickCaptchaView(
|
||||||
|
captchaData = captchaData!!,
|
||||||
|
onPositionClicked = onPositionClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
ActionButton(
|
||||||
|
text = "Refresh",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
onLoadCaptcha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
package com.aiosman.riderpro.ui.login
|
package com.aiosman.riderpro.ui.login
|
||||||
|
|
||||||
import android.icu.util.Calendar
|
|
||||||
import android.icu.util.TimeZone
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -16,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,35 +31,41 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.aiosman.riderpro.AppState
|
import com.aiosman.riderpro.AppState
|
||||||
import com.aiosman.riderpro.AppStore
|
import com.aiosman.riderpro.AppStore
|
||||||
|
import com.aiosman.riderpro.ErrorCode
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.Messaging
|
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.riderpro.R
|
||||||
import com.aiosman.riderpro.data.AccountService
|
import com.aiosman.riderpro.data.AccountService
|
||||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
import com.aiosman.riderpro.data.AccountServiceImpl
|
||||||
|
import com.aiosman.riderpro.data.CaptchaService
|
||||||
|
import com.aiosman.riderpro.data.CaptchaServiceImpl
|
||||||
import com.aiosman.riderpro.data.ServiceException
|
import com.aiosman.riderpro.data.ServiceException
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaInfo
|
||||||
|
import com.aiosman.riderpro.data.api.CaptchaResponseBody
|
||||||
|
import com.aiosman.riderpro.data.api.DotPosition
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
||||||
import com.aiosman.riderpro.ui.composables.ActionButton
|
import com.aiosman.riderpro.ui.composables.ActionButton
|
||||||
|
import com.aiosman.riderpro.ui.composables.ClickCaptchaDialog
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.riderpro.ui.composables.TextInputField
|
import com.aiosman.riderpro.ui.composables.TextInputField
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.riderpro.utils.GoogleLogin
|
import com.aiosman.riderpro.utils.GoogleLogin
|
||||||
import com.aiosman.riderpro.utils.Utils
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UserAuthScreen() {
|
fun UserAuthScreen() {
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("root") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("password") }
|
||||||
var rememberMe by remember { mutableStateOf(false) }
|
var rememberMe by remember { mutableStateOf(false) }
|
||||||
var accountService: AccountService = AccountServiceImpl()
|
val accountService: AccountService = AccountServiceImpl()
|
||||||
|
val captchaService: CaptchaService = CaptchaServiceImpl()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var emailError by remember { mutableStateOf<String?>(null) }
|
var emailError by remember { mutableStateOf<String?>(null) }
|
||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
|
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
|
||||||
fun validateForm(): Boolean {
|
fun validateForm(): Boolean {
|
||||||
emailError =
|
emailError =
|
||||||
if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
|
if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
|
||||||
@@ -72,13 +74,37 @@ fun UserAuthScreen() {
|
|||||||
return emailError == null && passwordError == null
|
return emailError == null && passwordError == null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLogin() {
|
var captchaData by remember { mutableStateOf<CaptchaResponseBody?>(null) }
|
||||||
|
fun loadLoginCaptcha() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
captchaData = captchaService.generateLoginCaptcha(email)
|
||||||
|
captchaData?.let {
|
||||||
|
captchaInfo = CaptchaInfo(
|
||||||
|
id = it.id,
|
||||||
|
dot = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: ServiceException) {
|
||||||
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLogin(captchaInfo: CaptchaInfo? = null) {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val authResp = accountService.loginUserWithPassword(email, password)
|
// 检查是否需要验证码
|
||||||
|
if (captchaInfo == null && captchaService.checkLoginCaptcha(email)) {
|
||||||
|
loadLoginCaptcha()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
// 获取用户凭证
|
||||||
|
val authResp = accountService.loginUserWithPassword(email, password, captchaInfo)
|
||||||
if (authResp.token != null) {
|
if (authResp.token != null) {
|
||||||
AppStore.apply {
|
AppStore.apply {
|
||||||
token = authResp.token
|
token = authResp.token
|
||||||
@@ -92,13 +118,25 @@ fun UserAuthScreen() {
|
|||||||
}
|
}
|
||||||
} catch (e: ServiceException) {
|
} catch (e: ServiceException) {
|
||||||
// handle error
|
// handle error
|
||||||
|
|
||||||
if (e.code == 12005) {
|
if (e.code == 12005) {
|
||||||
emailError = context.getString(R.string.error_invalidate_username_password)
|
emailError = context.getString(R.string.error_invalidate_username_password)
|
||||||
passwordError = context.getString(R.string.error_invalidate_username_password)
|
passwordError = context.getString(R.string.error_invalidate_username_password)
|
||||||
|
} else if (e.code == ErrorCode.InvalidateCaptcha.code) {
|
||||||
|
loadLoginCaptcha()
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"incorrect captcha,please try again",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
captchaData = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,10 +170,37 @@ fun UserAuthScreen() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
captchaData?.let {
|
||||||
|
ClickCaptchaDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
captchaData = null
|
||||||
|
},
|
||||||
|
captchaData = it,
|
||||||
|
onLoadCaptcha = {
|
||||||
|
loadLoginCaptcha()
|
||||||
|
},
|
||||||
|
onPositionClicked = { offset ->
|
||||||
|
captchaInfo?.let { info ->
|
||||||
|
val dots = info.dot.toMutableList()
|
||||||
|
val lastDotIndex = dots.size - 1
|
||||||
|
dots += DotPosition(
|
||||||
|
index = lastDotIndex + 1,
|
||||||
|
x = offset.x.toInt(),
|
||||||
|
y = offset.y.toInt()
|
||||||
|
)
|
||||||
|
captchaInfo = info.copy(dot = dots)
|
||||||
|
// 检查是否完成
|
||||||
|
if (dots.size == it.count) {
|
||||||
|
onLogin(captchaInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -204,26 +269,6 @@ fun UserAuthScreen() {
|
|||||||
navController.navigate(NavigationRoute.ResetPassword.route)
|
navController.navigate(NavigationRoute.ResetPassword.route)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
|
||||||
// Checkbox(
|
|
||||||
// checked = rememberMe,
|
|
||||||
// onCheckedChange = {
|
|
||||||
// rememberMe = it
|
|
||||||
// },
|
|
||||||
// colors = CheckboxDefaults.colors(
|
|
||||||
// checkedColor = Color.Black
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// Text(
|
|
||||||
// stringResource(R.string.remember_me),
|
|
||||||
// modifier = Modifier.padding(start = 8.dp),
|
|
||||||
// fontSize = 12.sp
|
|
||||||
// )
|
|
||||||
// Spacer(modifier = Modifier.weight(1f))
|
|
||||||
// Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable {
|
|
||||||
// navController.navigate(NavigationRoute.ResetPassword.route)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(64.dp))
|
Spacer(modifier = Modifier.height(64.dp))
|
||||||
|
|||||||
@@ -81,4 +81,5 @@
|
|||||||
<string name="like_your_comment">喜欢了你的评论</string>
|
<string name="like_your_comment">喜欢了你的评论</string>
|
||||||
<string name="resend">重新发送 %s</string>
|
<string name="resend">重新发送 %s</string>
|
||||||
<string name="error_40002_user_not_exist">用户不存在</string>
|
<string name="error_40002_user_not_exist">用户不存在</string>
|
||||||
|
<string name="captcha_hint">请依次点击图片中的元素</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -80,4 +80,5 @@
|
|||||||
<string name="like_your_comment">Like your comment</string>
|
<string name="like_your_comment">Like your comment</string>
|
||||||
<string name="resend">Resend in %s</string>
|
<string name="resend">Resend in %s</string>
|
||||||
<string name="error_40002_user_not_exist">user not exist</string>
|
<string name="error_40002_user_not_exist">user not exist</string>
|
||||||
|
<string name="captcha_hint">Please click on the dots in the image in the correct order.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user