ソースを参照

add(login): add login page

zhaoyadi 1 年間 前
コミット
d2aea13f5c

+ 1 - 0
.idea/gradle.xml

@@ -46,6 +46,7 @@
             <option value="$PROJECT_DIR$/feat/design" />
             <option value="$PROJECT_DIR$/feat/home" />
             <option value="$PROJECT_DIR$/feat/settings" />
+            <option value="$PROJECT_DIR$/feat/webview" />
           </set>
         </option>
       </GradleProjectSettings>

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -28,7 +28,7 @@
         <activity
             android:name="com.zaojiao.app.MainActivity"
             android:exported="true"
-            android:launchMode="singleInstance"
+            android:launchMode="singleTask"
             android:theme="@style/Theme.Luojigou.LightBar">
 
             <intent-filter>

+ 8 - 0
app/src/main/java/com/zaojiao/app/SplashActivity.kt

@@ -1,6 +1,7 @@
 package com.zaojiao.app
 
 import android.animation.ObjectAnimator
+import android.content.Intent
 import android.graphics.Color
 import android.graphics.drawable.ColorDrawable
 import android.os.Build
@@ -17,6 +18,7 @@ import androidx.core.animation.doOnEnd
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
+import kotlin.concurrent.thread
 
 class SplashActivity : AppCompatActivity() {
     private lateinit var windowInsetsController: WindowInsetsControllerCompat
@@ -37,6 +39,12 @@ class SplashActivity : AppCompatActivity() {
 
             setImageResource(R.mipmap.launch_image)
             setContentView(this)
+
+            thread {
+                Thread.sleep(3000)
+                startActivity(Intent(this@SplashActivity, MainActivity::class.java))
+                finish()
+            }
         }
     }
 

+ 1 - 1
core/auth/src/main/AndroidManifest.xml

@@ -5,7 +5,7 @@
         <activity
             android:name=".ui.TokenActivity"
             android:exported="true"
-            android:launchMode="singleInstance">
+            android:launchMode="singleTask">
 
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />

+ 6 - 1
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthApi.kt

@@ -6,7 +6,12 @@ import retrofit2.http.POST
 import retrofit2.http.Path
 
 interface AuthApi {
-    @POST("/app/sms/v2/checkCode/app/{phone}/{code}")
+    @GET("/app/sms/getCode/{phone}")
+    suspend fun getSmsCode(
+        @Path(value = "phone") phone: String,
+    ): SmsResult
+
+    @POST("/app/sms/checkCode/{phone}/{code}")
     suspend fun loginBySms(
         @Path(value = "phone") phone: String,
         @Path(value = "code") code: String,

+ 1 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthClient.kt

@@ -36,6 +36,7 @@ class AuthClient @Inject constructor(
 
     private val api = retrofit.create(AuthApi::class.java)
 
+    suspend fun getSmsCode(phone: String) = api.getSmsCode(phone)
     suspend fun loginBySms(phone: String, code: String) = api.loginBySms(phone, code)
 
     suspend fun loginByWechat(code: String) = api.loginByWechat(code)

+ 5 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthResult.kt

@@ -2,6 +2,11 @@ package com.zaojiao.app.core.auth.http
 
 import com.zaojiao.app.core.auth.model.TokenModel
 
+data class SmsResult(
+    val status: Int,
+    val msg: String?
+)
+
 data class AuthResult(
     val status: Int,
     val data: TokenModel?,

+ 312 - 17
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginActivity.kt

@@ -1,39 +1,89 @@
 package com.zaojiao.app.core.auth.ui
 
+import android.os.Build
 import android.os.Bundle
+import android.view.WindowManager
+import android.widget.Toast
 import androidx.activity.ComponentActivity
-import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.activity.OnBackPressedCallback
 import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 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.IntrinsicSize
+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.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material3.Icon
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+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.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+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.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
+import com.zaojiao.app.core.auth.R
 import com.zaojiao.app.core.auth.model.TokenModel
 import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 
 private val LocalTokenSave = compositionLocalOf<((token: TokenModel?) -> Unit)?> { null }
 
+private val LocalFinish = compositionLocalOf<(() -> Unit)?> { null }
+
 @AndroidEntryPoint
 class LoginActivity : ComponentActivity() {
+    private lateinit var windowInsetsController: WindowInsetsControllerCompat
     override fun onCreate(savedInstanceState: Bundle?) {
+        configureSystemBar()
         super.onCreate(savedInstanceState)
 
+        this.onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() {
+                TokenActivity.start(this@LoginActivity, null, null)
+                finish()
+            }
+        })
+
         setContent {
             val navController = rememberNavController()
 
@@ -41,7 +91,11 @@ class LoginActivity : ComponentActivity() {
                 LocalTokenSave provides {
                     TokenActivity.start(this@LoginActivity, it?.accessToken, it?.refreshToken)
                     finish()
-                }
+                },
+                LocalFinish provides {
+                    TokenActivity.start(this@LoginActivity, null, null)
+                    finish()
+                },
             ) {
                 MaterialTheme {
                     NavHost(
@@ -56,37 +110,278 @@ class LoginActivity : ComponentActivity() {
             }
         }
     }
+
+    private fun configureSystemBar() {
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+        windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
+
+        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+        window.statusBarColor = android.graphics.Color.TRANSPARENT
+        window.navigationBarColor = android.graphics.Color.TRANSPARENT
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            window.addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
+        }
+        windowInsetsController.apply {
+            show(WindowInsetsCompat.Type.systemBars())
+        }
+    }
 }
 
 @Composable
 fun LoginPage(
     loginViewModel: LoginViewModel = hiltViewModel(),
 ) {
+    val context = LocalContext.current
+    val callback = LocalTokenSave.current
+
+    val phone by loginViewModel.phone.collectAsState()
+    val smsCode by loginViewModel.smsCode.collectAsState()
+
     Column(
         modifier = Modifier
+            .background(color = Color.White)
             .statusBarsPadding()
             .fillMaxSize(),
     ) {
         CloseButton()
 
-        val callback = LocalTokenSave.current
 
-        Box(
+        Column(
             modifier = Modifier
-                .clickable {
-                    callback?.invoke(null)
-                }
-                .background(color = Color.Green)
                 .weight(1f)
-                .fillMaxWidth()
-        )
+                .fillMaxWidth(),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text(
+                text = "手机登录/注册",
+                style = TextStyle(
+                    color = Color(0xFF161616),
+                    fontSize = 24.sp,
+                    lineHeight = 24.sp,
+                ),
+                modifier = Modifier.align(Alignment.CenterHorizontally),
+            )
+            Box(modifier = Modifier.height(40.dp))
+            Row(
+                modifier = Modifier
+                    .background(
+                        color = Color(0xFFF7F7F7),
+                        shape = RoundedCornerShape(100.dp),
+                    )
+                    .width(284.dp)
+                    .height(48.dp)
+                    .padding(4.dp),
+            ) {
+                Box(modifier = Modifier.width(12.dp))
+                BasicTextField(
+                    value = phone,
+                    onValueChange = { value ->
+                        loginViewModel.updatePhone(value)
+                    },
+                    keyboardOptions = KeyboardOptions(
+                        keyboardType = KeyboardType.Number,
+                    ),
+                    textStyle = TextStyle(
+                        color = Color(0xFF333333),
+                    ),
+                    singleLine = true,
+                    modifier = Modifier
+                        .weight(1f)
+                        .align(Alignment.CenterVertically),
+                )
+                Box(modifier = Modifier.width(12.dp))
+
+                SendSmsCodeButton(
+                    enabled = phone.length == 11,
+                    onClick = {
+                        loginViewModel.sendSmsCode(
+                            onMessage = {
+                                Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
+                            },
+                        )
+                    }
+                )
+
+            }
+            Box(modifier = Modifier.height(12.dp))
+            Row(
+                modifier = Modifier
+                    .background(
+                        color = Color(0xFFF7F7F7),
+                        shape = RoundedCornerShape(100.dp),
+                    )
+                    .width(284.dp)
+                    .height(48.dp)
+                    .padding(4.dp),
+            ) {
+                Box(modifier = Modifier.width(12.dp))
+                BasicTextField(
+                    value = smsCode,
+                    onValueChange = { value ->
+                        loginViewModel.updateSmsCode(value)
+                    },
+                    keyboardOptions = KeyboardOptions(
+                        keyboardType = KeyboardType.Number,
+                    ),
+                    textStyle = TextStyle(
+                        color = Color(0xFF333333),
+                    ),
+                    singleLine = true,
+                    modifier = Modifier
+                        .weight(1f)
+                        .align(Alignment.CenterVertically),
+                )
+                Box(modifier = Modifier.width(12.dp))
+            }
+            Box(modifier = Modifier.height(32.dp))
+
+            val enableButton = phone.length == 11 && smsCode.length == 4
+
+            Box(
+                modifier = Modifier
+                    .clip(shape = RoundedCornerShape(100.dp))
+                    .clickable(
+                        enabled = enableButton
+                    ) {
+                        loginViewModel.requestLogin(
+                            onSuccess = {
+                                callback?.invoke(it)
+                            },
+                            onFailure = {
+                                Toast
+                                    .makeText(context, it, Toast.LENGTH_SHORT)
+                                    .show()
+                            },
+                        )
+                    }
+                    .background(
+                        color = if (enableButton) Color(0xFF0548BB) else Color(0xFFB7CAEC)
+                    )
+                    .height(42.dp)
+                    .width(284.dp),
+            ) {
+                Text(
+                    text = "登录", style = TextStyle(
+                        color = Color.White,
+                        fontSize = 16.sp,
+                        lineHeight = 18.sp,
+                        fontWeight = FontWeight.Medium,
+                    ), modifier = Modifier.align(Alignment.Center)
+                )
+            }
+        }
+        Column(
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally)
+                .navigationBarsPadding()
+                .padding(bottom = 16.dp)
+                .wrapContentHeight(),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Row(
+                modifier = Modifier
+                    .wrapContentWidth()
+                    .height(IntrinsicSize.Min),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                Box(
+                    modifier = Modifier
+                        .background(color = Color(0xFFF5F5F5))
+                        .size(40.dp, 2.dp),
+                )
+
+                Text(
+                    text = "其他登录方式",
+                    style = TextStyle(
+                        color = Color(0xFFBFBFBF),
+                        fontSize = 14.sp,
+                        lineHeight = 14.sp,
+                    ),
+                    modifier = Modifier.padding(horizontal = 8.dp),
+                )
+
+                Box(
+                    modifier = Modifier
+                        .background(color = Color(0xFFF5F5F5))
+                        .size(40.dp, 2.dp),
+                )
+            }
+            Box(modifier = Modifier.height(30.dp))
+            Image(
+                painter = painterResource(id = R.mipmap.login_wechat),
+                contentDescription = "",
+                modifier = Modifier.size(62.dp)
+            )
+            Box(modifier = Modifier.height(40.dp))
+            Text(
+                text = "我已阅读并接受《使用协议》和《隐私协议》",
+                style = TextStyle(
+                    color = Color(0xFF999999),
+                    fontSize = 12.sp,
+                    lineHeight = 22.sp,
+                ),
+            )
+        }
     }
 }
 
 @Composable
 fun CloseButton() {
-    Icon(
-        imageVector = Icons.Filled.Close,
-        contentDescription = "关闭按钮",
-    )
+    val finish = LocalFinish.current
+
+    Text(text = "取消", style = TextStyle(
+        color = Color(0xFF0548BB),
+        fontSize = 14.sp,
+        lineHeight = 20.sp,
+    ), modifier = Modifier
+        .clickable(
+            indication = null,
+            interactionSource = MutableInteractionSource(),
+        ) {
+            finish?.invoke()
+        }
+        .padding(horizontal = 16.dp, vertical = 16.dp))
+}
+
+@Composable
+private fun SendSmsCodeButton(
+    enabled: Boolean,
+    onClick: () -> Unit,
+) {
+    val coroutineScope = rememberCoroutineScope()
+    var clickState by remember { mutableStateOf(true) }
+    var countDown by remember { mutableStateOf(0) }
+
+    Box(
+        modifier = Modifier
+            .clip(shape = RoundedCornerShape(100.dp))
+            .clickable(enabled = enabled && clickState) {
+                coroutineScope.launch {
+                    clickState = false
+                    repeat(60) {
+                        countDown = 60 - it
+                        delay(1000)
+                    }
+                    clickState = true
+                }
+                onClick.invoke()
+            }
+            .background(color = if (enabled && clickState) Color(0xFF0548BB) else Color(0xFFB7CAEC))
+            .width(110.dp)
+            .fillMaxHeight(),
+    ) {
+        Text(
+            text = if (clickState) "获取验证码" else "${countDown}秒后重发",
+            style = TextStyle(
+                color = Color.White,
+                fontSize = 16.sp,
+                lineHeight = 16.sp,
+            ),
+            modifier = Modifier
+                .align(Alignment.Center)
+                .wrapContentSize(),
+        )
+    }
 }

+ 60 - 2
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginViewModel.kt

@@ -1,13 +1,71 @@
 package com.zaojiao.app.core.auth.ui
 
+import android.util.Log
 import androidx.lifecycle.ViewModel
-import com.zaojiao.app.core.auth.data.AuthRepository
+import androidx.lifecycle.viewModelScope
+import com.zaojiao.app.core.auth.http.AuthClient
+import com.zaojiao.app.core.auth.model.TokenModel
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
+sealed interface SmsCodeUiState {
+    object Disable : SmsCodeUiState
+    object Enable : SmsCodeUiState
+    data class Waiting(val seconds: Int) : SmsCodeUiState
+}
+
+sealed interface LoginBtnUiState {
+    object Enable : LoginBtnUiState
+
+    object Disable : LoginBtnUiState
+}
+
 @HiltViewModel
 class LoginViewModel @Inject constructor(
-    private val authRepository: AuthRepository,
+    private val authClient: AuthClient,
 ) : ViewModel() {
+    val phone = MutableStateFlow("")
+    val smsCode = MutableStateFlow("")
+
+    fun updatePhone(phone: String) = viewModelScope.launch {
+        var value = phone.filter { it in '0'..'9' }
+
+        if (value.length > 11) {
+            value = value.substring(0, 11)
+        }
+
+        this@LoginViewModel.phone.emit(value)
+    }
+
+    fun updateSmsCode(code: String) = viewModelScope.launch {
+        var value = code.filter { it in '0'..'9' }
+
+        if (value.length > 4) {
+            value = value.substring(0, 4)
+        }
+
+        this@LoginViewModel.smsCode.emit(value)
+    }
+
+    fun sendSmsCode(
+        onMessage: (String) -> Unit,
+    ) = viewModelScope.launch {
+        val result = authClient.getSmsCode(phone.value)
+        result.msg?.let { onMessage(it) }
+    }
+
+    fun requestLogin(
+        onSuccess: (TokenModel) -> Unit,
+        onFailure: (String) -> Unit,
+    ) = viewModelScope.launch {
+        val token = authClient.loginBySms(phone.value, smsCode.value)
 
+        if (token.status != 200) {
+            token.msg?.apply { onFailure(this) }
+        } else {
+            token.data?.apply { onSuccess(this) }
+        }
+    }
 }

+ 1 - 1
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenActivity.kt

@@ -21,7 +21,7 @@ class TokenActivity : AppCompatActivity() {
         fun start(context: Context, accessToken: String?, refreshToken: String?) {
             val intent = Intent(context, TokenActivity::class.java)
             intent.data =
-                Uri.parse("token://www.luojigou.vip/app?access_token=$accessToken&&refreshToken=$refreshToken")
+                Uri.parse("token://www.luojigou.vip/app?$KEY_ACCESS=$accessToken&$KEY_REFRESH=$refreshToken")
             context.startActivity(intent)
         }
     }

BIN
core/auth/src/main/res/mipmap-xhdpi/login_wechat.png


BIN
core/auth/src/main/res/mipmap-xxhdpi/login_wechat.png


BIN
core/auth/src/main/res/mipmap-xxxhdpi/login_wechat.png


+ 1 - 4
core/navx/src/main/java/com/zaojiao/app/core/navx/NavXUtils.kt

@@ -20,10 +20,7 @@ object NavXUtils {
 
     fun callWithLogin(callback: () -> Unit) {
         if (AuthUtils.state == AuthState.OUT) {
-            thread {
-                val result = AuthUtils.navToLogin().get()
-                Log.e("NavXUtils", "callWithLogin: $result")
-            }
+            AuthUtils.requestLogin()
         } else {
             callback()
         }

+ 5 - 2
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt

@@ -21,6 +21,7 @@ import androidx.compose.ui.layout.positionInParent
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.sp
+import com.zaojiao.app.core.navx.NavXUtils
 import com.zaojiao.app.feat.design.Colors
 import com.zaojiao.app.feat.home.plan.index.HomePlanIndexPage
 import com.zaojiao.app.feat.home.plan.total.HomePlanTotalPage
@@ -73,8 +74,10 @@ fun HomePlanPage() {
                 color = Colors.FF999999,
             ),
             onChange = {
-                coroutineScope.launch {
-                    pageState.scrollToPage(it)
+                NavXUtils.callWithLogin {
+                    coroutineScope.launch {
+                        pageState.scrollToPage(it)
+                    }
                 }
             },
             modifier = Modifier.onGloballyPositioned {

+ 1 - 0
settings.gradle.kts

@@ -39,3 +39,4 @@ include(":feat:home")
 include(":feat:baby")
 include(":feat:settings")
 include(":data:database")
+include(":feat:webview")