@file:Suppress("SameParameterValue") package com.img.rabbit.pages import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.util.Log import android.view.LayoutInflater import android.widget.CheckBox import android.widget.TextView import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.Checkbox import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.img.rabbit.R import com.img.rabbit.utils.AgreementTextHelper import com.img.rabbit.viewmodel.GeneralViewModel import com.img.rabbit.viewmodel.LoginViewModel import com.g.gysdk.EloginActivityParam import com.g.gysdk.GYManager import com.g.gysdk.GYResponse import com.g.gysdk.GyCallBack import com.img.rabbit.bean.local.toAlipayResult import com.img.rabbit.config.Constants.agreementUrl import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.pages.toolbar.TitleBar import com.img.rabbit.provider.storage.GlobalStateManager import com.img.rabbit.provider.storage.PreferenceUtil import com.img.rabbit.utils.StringUtils import com.img.rabbit.utils.UrlLinkUtils.openAgreement import kotlinx.coroutines.delay import org.json.JSONObject @SuppressLint("UnrememberedMutableState") @Composable fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel, isVisibilityBreak: Boolean) { val context = LocalContext.current val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true) var showNetworkDisconnected by remember { mutableStateOf(false) } // 网络状态监听 LaunchedEffect(networkStatus) { if (!networkStatus) { // 网络断开时的处理 Log.w("NetworkStatus","网络断开") }else{ Log.w("NetworkStatus","网络已连接") } } // 登录成功后,保存 token LaunchedEffect(loginViewModel.loginState.value) { if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) { //登录成功 PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token) loginViewModel.setLogin(true) //更新登录状态 GlobalStateManager(context).storeGlobalLoginNotify(true) //清理loginState loginViewModel.loginState.value = null //提示登录成功 Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show() // 当允许返回上一页时,登录成功后,返回上一页 if(isVisibilityBreak){ navController?.popBackStack() } }else if(loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token == null){ //登录失败 loginViewModel.setLogin(false) Log.w("LoginScreen","登录失败,无有效的Token") Toast.makeText(context, "登录失败,请重新登录", Toast.LENGTH_SHORT).show() } } var globalWxAuthorization by mutableStateOf(GlobalStateManager(context).globalWxAuthorizationFlow().collectAsState(initial = "")) LaunchedEffect(globalWxAuthorization.value) { if(globalWxAuthorization.value?.isNotEmpty() == true){ loginViewModel.requestWxLogin(globalWxAuthorization.value!!) //清理globalWxAuthorization GlobalStateManager(context).storeGlobalWxAuthorization("") } } LaunchedEffect(loginViewModel.authInfoForAlipay.value) { if(loginViewModel.authInfoForAlipay.value.isEmpty()) return@LaunchedEffect loginViewModel.loginWithAliPay(context){rawResult -> Log.i("loginWithAliPay", "支付宝登录结果:$rawResult") val alipayResult = rawResult.toAlipayResult() // 处理支付宝登录结果 when (alipayResult.resultStatus) { "9000" -> { // 登录成功,result 字段中包含 auth_code val authCode = StringUtils.parseAlipayResult(alipayResult.result ?: "")["auth_code"] ?: "" loginViewModel.requestAlipayLogin(authCode) } "6001" -> { "用户取消登录" } else -> { alipayResult.memo ?: "登录失败" } } //清理authInfoForAlipay loginViewModel.authInfoForAlipay.value = "" } } Scaffold{ Box( modifier = Modifier.fillMaxSize() ) { Image( painter = painterResource(id = R.mipmap.ic_main_previous_mask), contentDescription = null, contentScale = ContentScale.FillWidth, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) Column( modifier = Modifier .fillMaxSize() ) { Box(modifier = Modifier.fillMaxSize()){ when(loginViewModel.loginScreenType.value){ LoginScreenType.LOGIN_ONE_KEY -> { //一键登录 OneKeyLoginScreen(context, loginViewModel, generalViewModel) } LoginScreenType.LOGIN_WX -> { //微信登录 Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ WxLoginScreen(context, loginViewModel, generalViewModel) } } LoginScreenType.LOGIN_ALIPAY -> { //支付宝登录 Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){ AliPayLoginScreen(context, loginViewModel) } } else -> { //默认验证码登录 CaptchaLoginScreen(context, loginViewModel, generalViewModel) } } // 其他登录方式Bar Column ( modifier = Modifier .fillMaxSize() .padding(top = 27.dp), verticalArrangement = Arrangement.Bottom ){ OtherLoginBar(viewModel = loginViewModel) } } } LaunchedEffect(networkStatus) { delay(1000L) showNetworkDisconnected = true } if(showNetworkDisconnected){ if(!networkStatus){ NetworkDisconnectedPage(onNetworkStatus = {isNetworkAvailable-> if(isNetworkAvailable){ Toast.makeText(context, "网络已连接", Toast.LENGTH_SHORT).show() }else{ Toast.makeText(context, "网络已断开", Toast.LENGTH_SHORT).show() } generalViewModel.setNetworkStatus(isNetworkAvailable) }) } } // 顶部栏 TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak) } } } /** * 验证码登录 */ @Composable private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, generalViewModel: GeneralViewModel) { val gradientBrush = Brush.verticalGradient( colors = listOf( Color(0xFF91FEFA), // 浅蓝色 Color(0x0091FEFA) // 半透明 ), startY = 0f, endY = Float.POSITIVE_INFINITY ) // 验证码倒计时 var isCaptchaCountdown by remember { mutableStateOf(false) } // 倒计时秒数 var captchaCountdown by remember { mutableIntStateOf(60) } if (isCaptchaCountdown) { LaunchedEffect(key1 = isCaptchaCountdown) { while (captchaCountdown > 0) { delay(1000) // 等待1秒 captchaCountdown-- } isCaptchaCountdown = false captchaCountdown = 60 // 重置倒计时 } } Column { //登录标签 Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 38.dp, end = 38.dp, top = 150.dp) ) { Box( modifier = Modifier .width(4.dp) .height(55.dp) .background(gradientBrush) ) Column() { Text( text = "欢迎登录", modifier = Modifier.padding(start = 12.dp), fontSize =18.sp, fontWeight = FontWeight.Bold, color = Color(0xFF000000) ) Text( text = stringResource(R.string.app_name), modifier = Modifier.padding(start = 12.dp), fontSize =32.sp, fontWeight = FontWeight.Bold, color = Color(0xFF000000) ) } } //登录框 Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 38.dp, end = 38.dp, top = 80.dp) ) { // 用户名框(手机号) Box( modifier = Modifier .fillMaxWidth() .height(50.dp) .background( Color(0x4DE3E0ED), shape = RoundedCornerShape(12.dp), ) ){ Row( modifier = Modifier .fillMaxHeight() .fillMaxWidth() ) { Text( text = "+86", modifier = Modifier .padding(start = 14.dp, end = 12.dp) .align(Alignment.CenterVertically), fontSize =14.sp, fontWeight = FontWeight.Normal, color = Color(0xFF3D3D3D) ) Image( painter = painterResource(id = R.mipmap.ic_vertical_divider_dotted_line), contentDescription = null, modifier = Modifier .height(29.dp) .width(1.dp) .align(Alignment.CenterVertically) ) BasicTextField( value = viewModel.userName.value, onValueChange = { viewModel.setUserName(it) }, modifier = Modifier .fillMaxWidth() .background(Color.Transparent) .padding(horizontal = 12.dp) .align(Alignment.CenterVertically), textStyle = androidx.compose.ui.text.TextStyle( color = Color.Black, fontSize = 16.sp ), singleLine = true, maxLines = 1, keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( keyboardType = androidx.compose.ui.text.input.KeyboardType.Phone, imeAction = androidx.compose.ui.text.input.ImeAction.Done ), decorationBox = { innerTextField -> Box( modifier = Modifier .fillMaxWidth() .background(Color.Transparent) ) { if (viewModel.userName.value.isEmpty()) { Text( "请输入手机号", color = Color.Gray, fontSize = 16.sp ) } innerTextField() } }, ) } } // 分割线 Box( modifier = Modifier .fillMaxWidth() .height(20.dp) .background(Color.Transparent) ) // 验证码框 Box( modifier = Modifier .fillMaxWidth() .height(50.dp) .background( Color(0x4DE3E0ED), shape = RoundedCornerShape(12.dp), ) ) { Row( modifier = Modifier .fillMaxHeight() .fillMaxWidth() .padding(end = 5.dp), horizontalArrangement = Arrangement.End ) { BasicTextField( value = viewModel.captcha.value, onValueChange = { viewModel.setCaptcha(it) }, modifier = Modifier .weight(1f) .align(Alignment.CenterVertically) .background(Color.Transparent), textStyle = androidx.compose.ui.text.TextStyle( color = Color.Black, fontSize = 16.sp ), singleLine = true, maxLines = 1, keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( keyboardType = androidx.compose.ui.text.input.KeyboardType.Text, imeAction = androidx.compose.ui.text.input.ImeAction.Done ), decorationBox = { innerTextField -> Box( modifier = Modifier .fillMaxWidth() .align(Alignment.CenterVertically) .padding(horizontal = 14.dp) .background(Color.Transparent) ) { if (viewModel.captcha.value.isEmpty()) { Text( "请输入验证码", color = Color.Gray, fontSize = 16.sp ) } innerTextField() } } ) Box( modifier = Modifier .height(40.dp) .width(98.dp) .align(Alignment.CenterVertically) .background( if (isCaptchaCountdown) Color(0xFFE0E0E0) else Color(0xFFFFFFFF), shape = RoundedCornerShape(8.dp) ) .clickable(enabled = !isCaptchaCountdown) { // 点击获取验证码 if (validatePhoneEmpty( context = context, viewModel = viewModel, showToast = true ) ) { // 请求验证码(请完善requestCaptcha函数) viewModel.requestCaptcha(viewModel.userName.value) // 开始倒计时(倒计时应该在requestCaptcha完成后开始) isCaptchaCountdown = true return@clickable } } ){ if (isCaptchaCountdown) { // 倒计时文本(倒计时从60秒开始) Text( "${captchaCountdown}秒", color = Color(0xFF3D3D3D), fontSize = 12.sp, modifier = Modifier .align(Alignment.Center) ) } else { Text( "获取验证码", color = Color(0xFF3D3D3D), fontSize = 12.sp, modifier = Modifier .align(Alignment.Center) ) } } } } } // 登录按钮 Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 46.dp) .background( Color(0xFF252525), shape = RoundedCornerShape(359.dp), ) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 点击登录 if (validateCaptchaLoginEmpty( context = context, viewModel = viewModel, showToast = true ) ) { // 验证通过(通过验证码验证),请求登录 viewModel.requestLoginForCaptcha( viewModel.userName.value, viewModel.captcha.value ) } } ) { Text( "登录", color = Color(0xFFC2FF43), fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier .padding(vertical = 12.dp) .align(Alignment.Center) ) } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 14.dp) ) { Checkbox( checked = viewModel.isPolicyAgreement.value, onCheckedChange = { isChecked -> viewModel.setIsPolicyAgreement(isChecked) }, modifier = Modifier .size(16.dp) .scale(0.35f) .padding(start = 6.dp) .background( if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color.Transparent, shape = RoundedCornerShape(36.dp) ) .border( width = 1.dp, color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color(0xFFCCCCCC), shape = RoundedCornerShape(36.dp) ) .align(Alignment.CenterVertically), colors = androidx.compose.material3.CheckboxDefaults.colors( checkedColor = Color.Transparent, // 隐藏默认背景 uncheckedColor = Color.Transparent, // 隐藏默认背景 checkmarkColor = Color.White ) ) val annotatedText = buildAnnotatedString { append("我已阅读并同意") // 用户协议部分 pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《用户协议》") } pop() append("和") // 隐私政策部分 pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《隐私政策》") } pop() } ClickableText( text = annotatedText, onClick = { offset -> annotatedText.getStringAnnotations(offset, offset) .firstOrNull()?.let { annotation -> when (annotation.tag) { "USER_AGREEMENT" -> { // 打开用户协议 openAgreement(context = context, title = "用户协议", url = agreementUrl) } "PRIVACY_POLICY" -> { // 打开隐私政策 openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } } }, style = androidx.compose.ui.text.TextStyle( fontSize = 12.sp, color = Color.Gray ), modifier = Modifier .padding(start = 4.dp) .align(Alignment.CenterVertically) ) } } } /** * 一键登录页 * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) */ @SuppressLint("UseKtx", "InflateParams", "SetTextI18n") @Composable private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, generalViewModel: GeneralViewModel) { val ONEKEY_TAG = "OneKeyLoginScreen" val preLoginResult = GYManager.getInstance().preLoginResult val phoneNumber = viewModel.oneKeyPreLogin?.number ?: "" val operator = preLoginResult.operator//运营商,CM 移动,CT 电信,CU 联通 val privacyName = "《${preLoginResult.privacyName}》" val privacyUrl = preLoginResult.privacyUrl // 详细打印preLoginResult信息 Log.w(ONEKEY_TAG, "=== preLoginResult 详细信息 ===") Log.w(ONEKEY_TAG, "preLoginResult对象: $preLoginResult") Log.w(ONEKEY_TAG, "operator: ${preLoginResult.operator}") Log.w(ONEKEY_TAG, "isValid: ${preLoginResult.isValid}") Log.w(ONEKEY_TAG, "privacyName: ${preLoginResult.privacyName}") Log.w(ONEKEY_TAG, "privacyUrl: ${preLoginResult.privacyUrl}") Log.w(ONEKEY_TAG, "================================") val agreementText = "登录即认可${privacyName}、《用户协议》和《隐私政策》并使用本机号码登录" Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) { // 使用AndroidView嵌入XML布局 @Suppress("COMPOSE_APPLIER_CALL_MISMATCH") AndroidView( factory = { context -> LayoutInflater.from(context).inflate( R.layout.layout_one_key_login, null, false ) as ConstraintLayout }, modifier = Modifier.fillMaxSize(), update = { view -> // 动态更新XML布局中的内容 val phoneTextView = view.findViewById(R.id.layout_one_key_login_tv_phone) phoneTextView.text = phoneNumber // 需要回去手机号码 val serviceTextView = view.findViewById(R.id.layout_one_key_login_tv_service) val serviceText = when (operator) { "CT" -> {//运营商,CM 移动,CT 电信,CU 联通 "天翼账号提供认证服务" } "CU" -> {//联通 "联通账号提供认证服务" } else -> {//移动 "移动账号提供认证服务" } } serviceTextView.text = serviceText // 品牌露出,如:“天翼账号提供认证服务” val checkbox = view.findViewById(R.id.layout_one_key_login_agreement_checkbox) checkbox.isChecked = viewModel.isPolicyAgreement.value checkbox.setOnCheckedChangeListener { _, isChecked -> viewModel.setIsPolicyAgreement(isChecked) } val agreementTextView = view.findViewById(R.id.layout_one_key_login_agreement_tv) //TODO 服务协议,如:“登录即认可《天翼账号服务与隐私协议》、《用户协议》和《隐私政策》并使用本机号码登录” val targets = mapOf( "serviceAgreement" to privacyName, "userAgreement" to "《用户协议》", "privacyAgreement" to "《隐私政策》" ) // 设置可点击的协议文本 AgreementTextHelper.setupAgreementTextView(agreementText, targets, agreementTextView, isUnderlineText = false) { agreementType -> when (agreementType) { "serviceAgreement" -> openAgreement(context = context, title = privacyName, url = privacyUrl) "userAgreement" -> openAgreement(context = context, title = "用户协议", url = agreementUrl) "privacyAgreement" -> openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } val loginButton = view.findViewById(R.id.layout_one_key_login_btn) loginButton.setOnClickListener { // 处理登录点击 oneKeyLogin( context = context, numberTv = phoneTextView, sloganTv = serviceTextView, loginBtn = loginButton, checkBox = checkbox, privacyTv = agreementTextView, viewModel = viewModel ) } } ) } } @Composable private fun WxLoginScreen( context: Context, viewModel: LoginViewModel, generalViewModel: GeneralViewModel ) { Column { Image( painter = painterResource(id = R.mipmap.ic_launcher_logo), contentDescription = null, modifier = Modifier .size(86.dp) .align(Alignment.CenterHorizontally) ) Text( text = "截图兔", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color(0xFF1A1A1A), modifier = Modifier .wrapContentWidth() .wrapContentHeight() .align(Alignment.CenterHorizontally) ) Text( text = "为了更好地为您提供服务,请先完成微信授权", fontWeight = FontWeight.Bold, fontSize = 12.sp, color = Color(0xFFAAAAAA), modifier = Modifier .wrapContentWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 46.dp) .align(Alignment.CenterHorizontally) ) // 登录按钮 Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 8.dp) .background( Color(0xFF252525), shape = RoundedCornerShape(359.dp), ) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 启动微信验证,请求登录 if (viewModel.isPolicyAgreement.value) { //打开微信登录 viewModel.loginWithWechat(context, generalViewModel.api) } else { Toast.makeText( context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT ).show() } } ) { Row( modifier = Modifier .align(Alignment.Center) ) { Image( painter = painterResource(id = R.drawable.ic_wx_icon), contentDescription = null, modifier = Modifier .size(32.dp) .padding(start = 12.dp) .align(Alignment.CenterVertically) ) Text( "微信授权登录", color = Color(0xFFC2FF43), fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier .padding(vertical = 12.dp) ) } } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 14.dp) ) { Checkbox( checked = viewModel.isPolicyAgreement.value, onCheckedChange = { isChecked -> viewModel.setIsPolicyAgreement(isChecked) }, modifier = Modifier .size(16.dp) .scale(0.35f) .padding(start = 6.dp) .background( if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color.Transparent, shape = RoundedCornerShape(36.dp) ) .border( width = 1.dp, color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color(0xFFCCCCCC), shape = RoundedCornerShape(36.dp) ) .align(Alignment.CenterVertically), colors = androidx.compose.material3.CheckboxDefaults.colors( checkedColor = Color.Transparent, // 隐藏默认背景 uncheckedColor = Color.Transparent, // 隐藏默认背景 checkmarkColor = Color.White ) ) val annotatedText = buildAnnotatedString { append("我已阅读并同意") // 用户协议部分 pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《用户协议》") } pop() append("和") // 隐私政策部分 pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《隐私政策》") } pop() } ClickableText( text = annotatedText, onClick = { offset -> annotatedText.getStringAnnotations(offset, offset) .firstOrNull()?.let { annotation -> when (annotation.tag) { "USER_AGREEMENT" -> { // 打开用户协议 openAgreement(context = context, title = "用户协议", url = agreementUrl) } "PRIVACY_POLICY" -> { // 打开隐私政策 openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } } }, style = androidx.compose.ui.text.TextStyle( fontSize = 12.sp, color = Color.Gray ), modifier = Modifier .padding(start = 4.dp) .align(Alignment.CenterVertically) ) } } } @Composable private fun AliPayLoginScreen( context: Context, viewModel: LoginViewModel, ) { Column{ Image( painter = painterResource(id = R.mipmap.ic_launcher_logo), contentDescription = null, modifier = Modifier .size(86.dp) .align(Alignment.CenterHorizontally) ) Text( text = "截图兔", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color(0xFF1A1A1A), modifier = Modifier .wrapContentWidth() .wrapContentHeight() .align(Alignment.CenterHorizontally) ) Text( text = "为了更好地为您提供服务,请先完成支付宝授权", fontWeight = FontWeight.Bold, fontSize = 12.sp, color = Color(0xFFAAAAAA), modifier = Modifier .wrapContentWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 46.dp) .align(Alignment.CenterHorizontally) ) // 登录按钮 Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 8.dp) .background( Color(0xFF252525), shape = RoundedCornerShape(359.dp), ) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 启动支付宝验证,请求登录 if (viewModel.isPolicyAgreement.value) { // 打开支付宝登录 viewModel.requestAliPayAuthParam() } else { Toast.makeText( context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT ).show() } } ) { Row( modifier = Modifier .align(Alignment.Center) ) { Image( painter = painterResource(id = R.drawable.ic_alipay_icon), contentDescription = null, modifier = Modifier .size(32.dp) .padding(start = 12.dp) .align(Alignment.CenterVertically) ) Text( "支付宝授权登录", color = Color(0xFFC2FF43), fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier .padding(vertical = 12.dp) ) } } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 14.dp) ) { Checkbox( checked = viewModel.isPolicyAgreement.value, onCheckedChange = { isChecked -> viewModel.setIsPolicyAgreement(isChecked) }, modifier = Modifier .size(16.dp) .scale(0.35f) .padding(start = 6.dp) .background( if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color.Transparent, shape = RoundedCornerShape(36.dp) ) .border( width = 1.dp, color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color(0xFFCCCCCC), shape = RoundedCornerShape(36.dp) ) .align(Alignment.CenterVertically), colors = androidx.compose.material3.CheckboxDefaults.colors( checkedColor = Color.Transparent, // 隐藏默认背景 uncheckedColor = Color.Transparent, // 隐藏默认背景 checkmarkColor = Color.White ) ) val annotatedText = buildAnnotatedString { append("我已阅读并同意") // 用户协议部分 pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《用户协议》") } pop() append("和") // 隐私政策部分 pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《隐私政策》") } pop() } ClickableText( text = annotatedText, onClick = { offset -> annotatedText.getStringAnnotations(offset, offset) .firstOrNull()?.let { annotation -> when (annotation.tag) { "USER_AGREEMENT" -> { // 打开用户协议 openAgreement(context = context, title = "用户协议", url = agreementUrl) } "PRIVACY_POLICY" -> { // 打开隐私政策 openAgreement(context = context, title = "隐私政策", url = privacyUrl) } } } }, style = androidx.compose.ui.text.TextStyle( fontSize = 12.sp, color = Color.Gray ), modifier = Modifier .padding(start = 4.dp) .align(Alignment.CenterVertically) ) } } } /** * 一键登录页 * 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview) */ /* @SuppressLint("UseKtx") @Composable private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) { val preLoginResult = GYManager.getInstance().preLoginResult preLoginResult.operator preLoginResult.isValid Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(top = 230.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = "18698756851", fontWeight = FontWeight.Bold, fontSize = 36.sp, color = Color(0xFF1A1A1A), modifier = Modifier .wrapContentWidth() .wrapContentHeight(), ) Text( text = "天翼账号提供认证服务", fontWeight = FontWeight.Normal, fontSize = 14.sp, color = Color(0xFF767676), modifier = Modifier .wrapContentWidth() .wrapContentHeight(), ) // 登录按钮 Box( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 46.dp) .background( Color(0xFF252525), shape = RoundedCornerShape(359.dp), ) .clickable { // 点击登录 if (validateCaptchaLoginEmpty( context = context, viewModel = viewModel, showToast = true ) ) { //TODO 验证码登录请求 Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show() } } ) { Text( "本机号码一键绑定", color = Color(0xFFC2FF43), fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier .padding(vertical = 12.dp) .align(Alignment.Center) ) } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 30.dp, end = 30.dp, top = 14.dp) ) { Checkbox( checked = viewModel.isPolicyAgreement.value, onCheckedChange = { isChecked -> viewModel.setIsPolicyAgreement(isChecked) }, modifier = Modifier .size(16.dp) .scale(0.35f) .padding(top = 16.dp, start = 6.dp) .background( if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color.Transparent, shape = RoundedCornerShape(36.dp) ) .border( width = 1.dp, color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525) else Color(0xFFCCCCCC), shape = RoundedCornerShape(36.dp) ), colors = androidx.compose.material3.CheckboxDefaults.colors( checkedColor = Color.Transparent, // 隐藏默认背景 uncheckedColor = Color.Transparent, // 隐藏默认背景 checkmarkColor = Color.White ) ) val annotatedText = buildAnnotatedString { append("我已阅读并同意") // 用户协议部分 pushStringAnnotation(tag = "SERVICE_AGREEMENT", annotation = "service_agreement") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("${preLoginResult.privacyName}") } pop() append("、") // 用户协议部分 pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《用户协议》") } pop() append("和") // 隐私政策部分 pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy") withStyle(style = SpanStyle( color = Color(0xFF767676), fontWeight = FontWeight.Bold, //textDecoration = TextDecoration.Underline )) { append("《隐私政策》") } pop() } ClickableText( text = annotatedText, onClick = { offset -> annotatedText.getStringAnnotations(offset, offset) .firstOrNull()?.let { annotation -> when (annotation.tag) { "SERVICE_AGREEMENT" -> { // 打开服务协议 preLoginResult.privacyUrl?.let { Intent(Intent.ACTION_VIEW, it.toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }.let { intent -> context.startActivity(intent) } } } "USER_AGREEMENT" -> { //TODO 打开用户协议 Toast.makeText(context, "打开用户协议", Toast.LENGTH_SHORT).show() } "PRIVACY_POLICY" -> { //TODO 打开隐私政策 Toast.makeText(context, "打开隐私政策", Toast.LENGTH_SHORT).show() } } } }, style = androidx.compose.ui.text.TextStyle( fontSize = 12.sp, color = Color.Gray ), modifier = Modifier .padding(start = 4.dp) .align(Alignment.CenterVertically) ) } } } */ @Composable private fun OtherLoginBar(viewModel: LoginViewModel) { Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(start = 64.dp, end = 64.dp) ) { Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Box( modifier = Modifier .width(60.dp) .height(2.dp) .align(Alignment.CenterVertically) .background( brush = Brush.horizontalGradient( colors = listOf(Color(0x00D8D8D8), Color(0xFFE5E5E5)) ) ) ) Text( "其他登录方式", color = Color(0xFF767676), fontSize = 14.sp, fontWeight = FontWeight.Normal, modifier = Modifier .padding(horizontal = 14.dp) .align(Alignment.CenterVertically) ) Box( modifier = Modifier .width(60.dp) .height(2.dp) .align(Alignment.CenterVertically) .background( brush = Brush.horizontalGradient( colors = listOf(Color(0xFFE5E5E5), Color(0x00D8D8D8)) ) ) ) } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(horizontal = 6.dp, vertical = 27.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Image( painter = painterResource(id = R.mipmap.ic_wechat), contentDescription = null, modifier = Modifier .size(32.dp) .weight(1f) .align(Alignment.CenterVertically) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 微信登录 viewModel.loginScreenType.value = LoginScreenType.LOGIN_WX } ) Image( painter = painterResource(id = R.mipmap.ic_alipay), contentDescription = null, modifier = Modifier .size(32.dp) .weight(1f) .align(Alignment.CenterVertically) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 支付宝登录 viewModel.loginScreenType.value = LoginScreenType.LOGIN_ALIPAY } ) Image( painter = painterResource(id = R.mipmap.ic_phone), contentDescription = null, modifier = Modifier .size(32.dp) .weight(1f) .align(Alignment.CenterVertically) .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) { // 校验码登录 viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA } ) } } } /** * 验证手机号是否为空 */ private fun validatePhoneEmpty(context: Context, viewModel: LoginViewModel, showToast: Boolean = false): Boolean { if (showToast && viewModel.userName.value.isEmpty()) { Toast.makeText(context, "请输入手机号", Toast.LENGTH_SHORT).show() } return viewModel.userName.value.isNotEmpty() } /** * 验证验证码登录是否为空 */ private fun validateCaptchaLoginEmpty(context: Context, viewModel: LoginViewModel, showToast: Boolean = false): Boolean { if (showToast) { if(viewModel.userName.value.isEmpty()){ Toast.makeText(context, "请输入手机号", Toast.LENGTH_SHORT).show() }else if(viewModel.captcha.value.isEmpty()){ Toast.makeText(context, "请输入验证码", Toast.LENGTH_SHORT).show() }else if(!viewModel.isPolicyAgreement.value){ Toast.makeText(context, "请同意用户协议和隐私政策", Toast.LENGTH_SHORT).show() } } return viewModel.userName.value.isNotEmpty() && viewModel.captcha.value.isNotEmpty() && viewModel.isPolicyAgreement.value } private fun oneKeyLogin( context: Context, numberTv: TextView, sloganTv: TextView, loginBtn: TextView, checkBox: CheckBox, privacyTv: TextView, viewModel: LoginViewModel, ) { val eloginActivityParam = EloginActivityParam() .setActivity(context as Activity) .setNumberTextview(numberTv) .setSloganTextview(sloganTv) .setLoginButton(loginBtn) .setPrivacyCheckbox(checkBox) .setPrivacyTextview(privacyTv) .setUiErrorListener { msg -> //隐私协议未打勾、界面不合规、setLoginOnClickListener抛出异常等情况下的回调 Log.e("OneKeyLogin", "UIErrorListener.onError:$msg") } .setLoginOnClickListener { if (!checkBox.isChecked) { // 抛出异常,避免sdk进行后续登录动作(否则eAccountLogin会回调onFailed错误) throw IllegalStateException("请先仔细阅读协议并勾选,然后再点击登录") } //启动登录时候的转圈圈 } GYManager.getInstance().eAccountLogin(eloginActivityParam, 5000, object : GyCallBack { override fun onSuccess(response: GYResponse?) { // 登录成功,需要与后端交互 Log.i("OneKeyLogin", "onSuccess:$response") try { val jsonObject = JSONObject(response?.msg?:"{}") val data = jsonObject.getJSONObject("data") val token = data.getString("token") viewModel.requestOneKeyLogin(response?.gyuid?:"", token) } catch (e: Exception) { e.printStackTrace() } } override fun onFailed(p0: GYResponse?) { // 登录失败 Log.e("OneKeyLogin", "onFailed:$p0") } }) } enum class LoginScreenType { LOGIN_NORMAL, LOGIN_ONE_KEY, LOGIN_CAPTCHA, LOGIN_WX, LOGIN_ALIPAY, } @Preview(showBackground = true) @Composable private fun PreviewOneKeyLoginScreen() { OneKeyLoginScreen(LocalContext.current, viewModel(), viewModel()) } @Preview(showBackground = true) @Composable private fun PreviewWxLoginScreen() { WxLoginScreen(LocalContext.current, viewModel(), viewModel()) } @Preview(showBackground = true) @Composable private fun PreviewAliPayLoginScreen() { AliPayLoginScreen(LocalContext.current, viewModel()) } @Preview(showBackground = true) @Composable private fun PreviewLoginScreen() { LoginScreen(navController = rememberNavController(), generalViewModel = viewModel(), loginViewModel = viewModel(), isVisibilityBreak = false) }