2 Commits 6b4bf14b4c ... 687521a37d

Author SHA1 Message Date
  zhaoyadi 687521a37d add(webview): add webview 1 year ago
  zhaoyadi d2aea13f5c add(login): add login page 1 year ago
28 changed files with 626 additions and 56 deletions
  1. 1 0
      .idea/gradle.xml
  2. 1 1
      app/src/main/AndroidManifest.xml
  3. 8 0
      app/src/main/java/com/zaojiao/app/SplashActivity.kt
  4. 1 1
      core/auth/src/main/AndroidManifest.xml
  5. 6 1
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthApi.kt
  6. 1 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthClient.kt
  7. 5 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthResult.kt
  8. 312 17
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginActivity.kt
  9. 60 2
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginViewModel.kt
  10. 1 1
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenActivity.kt
  11. 0 22
      core/auth/src/main/res/layout/layout_login.xml
  12. BIN
      core/auth/src/main/res/mipmap-xhdpi/login_wechat.png
  13. BIN
      core/auth/src/main/res/mipmap-xxhdpi/login_wechat.png
  14. BIN
      core/auth/src/main/res/mipmap-xxxhdpi/login_wechat.png
  15. 1 4
      core/navx/src/main/java/com/zaojiao/app/core/navx/NavXUtils.kt
  16. 2 0
      feat/design/build.gradle.kts
  17. 10 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/viewmodel/BaseViewModel.kt
  18. 0 4
      feat/home/build.gradle.kts
  19. 5 2
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt
  20. 24 0
      feat/webview/build.gradle.kts
  21. 9 0
      feat/webview/src/main/AndroidManifest.xml
  22. 25 0
      feat/webview/src/main/java/com/zaojiao/app/feat/webview/JavaScriptChannel.kt
  23. 6 0
      feat/webview/src/main/java/com/zaojiao/app/feat/webview/LJGWebViewClient.kt
  24. 43 0
      feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewActivity.kt
  25. 35 0
      feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewHandler.kt
  26. 26 0
      feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewMessage.kt
  27. 42 0
      feat/webview/src/main/res/layout/layout_webview.xml
  28. 2 1
      settings.gradle.kts

+ 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)
         }
     }

+ 0 - 22
core/auth/src/main/res/layout/layout_login.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <EditText
-        android:id="@+id/edit_phone"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <EditText
-        android:id="@+id/edit_sms"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <Button
-        android:id="@+id/action_login"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="登录" />
-</LinearLayout>

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()
         }

+ 2 - 0
feat/design/build.gradle.kts

@@ -9,6 +9,8 @@ android {
 }
 
 dependencies {
+    api(project(":feat:webview"))
+
     implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
 
     implementation(libs.coil.kt.compose)

+ 10 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/viewmodel/BaseViewModel.kt

@@ -25,6 +25,16 @@ abstract class BaseViewModel<T> : ViewModel() {
         }
     }
 
+    fun refreshState() = viewModelScope.launch {
+        try {
+            val result = productState()
+            setSuccess(result)
+        } catch (e: Throwable) {
+            Log.e("BaseViewModel", "${this.javaClass.simpleName}: error", e)
+            setFailure(0)
+        }
+    }
+
     abstract suspend fun productState(): T
 
     protected suspend fun setLoading() {

+ 0 - 4
feat/home/build.gradle.kts

@@ -23,8 +23,4 @@ dependencies {
     implementation(project(":feat:design"))
     implementation(project(":feat:baby"))
     implementation(project(":feat:settings"))
-}
-
-tasks.assembleAndroidTest{
-
 }

+ 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 {

+ 24 - 0
feat/webview/build.gradle.kts

@@ -0,0 +1,24 @@
+plugins {
+    id("d.convention.library")
+    kotlin("plugin.serialization")
+}
+
+android {
+    namespace = "com.zaojiao.app.feat.webview"
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+        }
+    }
+}
+
+dependencies {
+    implementation("androidx.appcompat:appcompat:1.6.1")
+
+    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
+}

+ 9 - 0
feat/webview/src/main/AndroidManifest.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+        <activity android:name=".WebViewActivity">
+
+        </activity>
+    </application>
+</manifest>

+ 25 - 0
feat/webview/src/main/java/com/zaojiao/app/feat/webview/JavaScriptChannel.kt

@@ -0,0 +1,25 @@
+package com.zaojiao.app.feat.webview
+
+import android.os.Looper
+import android.webkit.JavascriptInterface
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
+import java.util.logging.Handler
+
+
+class JavaScriptChannel(
+    private val webViewHandler: WebViewHandler
+) {
+    private val json = Json {
+        ignoreUnknownKeys = true
+    }
+
+    @JavascriptInterface
+    fun postMessage(message: String) {
+        try {
+            webViewHandler.callHandler(json.decodeFromString(message))
+        } catch (exception: SerializationException) {
+            exception.printStackTrace()
+        }
+    }
+}

+ 6 - 0
feat/webview/src/main/java/com/zaojiao/app/feat/webview/LJGWebViewClient.kt

@@ -0,0 +1,6 @@
+package com.zaojiao.app.feat.webview
+
+import android.webkit.WebViewClient
+
+class LJGWebViewClient:WebViewClient() {
+}

+ 43 - 0
feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewActivity.kt

@@ -0,0 +1,43 @@
+package com.zaojiao.app.feat.webview
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.webkit.WebView
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+
+class WebViewActivity : AppCompatActivity() {
+    companion object {
+        fun start(context: Context, url: String) {
+            val intent = Intent(context, WebViewActivity::class.java)
+            intent.data = Uri.parse(url)
+            context.startActivity(intent)
+        }
+    }
+
+    private lateinit var actionBar: LinearLayout
+    private lateinit var actionBack: ImageView
+    private lateinit var actionTitle: TextView
+    private lateinit var webView: WebView
+
+    private lateinit var handler: WebViewHandler
+
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.layout_webview)
+
+        actionBar = findViewById(R.id.action_bar)
+        actionBack = findViewById(R.id.action_back)
+        actionTitle = findViewById(R.id.action_title)
+        webView = findViewById(R.id.content_webview)
+
+        handler = WebViewHandler(webView)
+
+        webView.loadUrl(intent.data.toString())
+    }
+}

+ 35 - 0
feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewHandler.kt

@@ -0,0 +1,35 @@
+package com.zaojiao.app.feat.webview
+
+import android.webkit.WebView
+
+class WebViewHandler(
+    private val webView: WebView,
+) {
+    private val handlers = mutableMapOf<String, (WebViewRequest) -> WebViewResponse>()
+
+    init {
+        webView.addJavascriptInterface(JavaScriptChannel(this), "jsBridge")
+    }
+
+    @Throws(IllegalArgumentException::class)
+    fun register(callbackName: String, executor: (WebViewRequest) -> WebViewResponse) {
+        if (handlers[callbackName] != null) {
+            throw IllegalArgumentException("$callbackName 已经注册过了")
+        }
+
+        handlers[callbackName] = executor
+    }
+
+    fun unregister(callName: String) {
+        handlers.remove(callName)
+    }
+
+    @Throws(IllegalArgumentException::class)
+    fun callHandler(request: WebViewRequest) {
+        val executor = handlers[request.callbackName]
+            ?: throw IllegalArgumentException("${request.callbackName} 还未注册")
+
+        val response = executor.invoke(request)
+        webView.evaluateJavascript("javascript:${request.callbackName}", null)
+    }
+}

+ 26 - 0
feat/webview/src/main/java/com/zaojiao/app/feat/webview/WebViewMessage.kt

@@ -0,0 +1,26 @@
+package com.zaojiao.app.feat.webview
+
+data class WebViewRequest(
+    /* 回调的方法 */
+    val callbackName: String,
+    /* 回调的id */
+    val callbackId: String,
+    val payload: Any?,
+)
+
+
+data class WebViewResponse(
+    val callbackName: String,
+    val callbackId: String,
+    val data: Any?,
+    val error: Any?,
+)
+
+fun WebViewRequest.response(data: Any?, error: Any?): WebViewResponse {
+    return WebViewResponse(
+        callbackName = this.callbackName,
+        callbackId = this.callbackId,
+        data = data,
+        error = error,
+    )
+}

+ 42 - 0
feat/webview/src/main/res/layout/layout_webview.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/white"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/action_bar"
+        android:layout_width="match_parent"
+        android:layout_height="44dp"
+        android:background="@android:color/transparent">
+
+        <ImageView
+            android:id="@+id/action_back"
+            android:layout_width="44dp"
+            android:layout_height="44dp" />
+
+        <TextView
+            android:id="@+id/action_title"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            android:gravity="center"
+            android:paddingHorizontal="16dp"
+            android:text="hello"
+            android:textAlignment="center"
+            android:textColor="@android:color/black"
+            android:textSize="16sp" />
+
+        <View
+            android:layout_width="44dp"
+            android:layout_height="44dp" />
+    </LinearLayout>
+
+    <WebView
+        android:id="@+id/content_webview"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+</LinearLayout>

+ 2 - 1
settings.gradle.kts

@@ -29,6 +29,7 @@ include(":core:nav")
 include(":core:navx")
 
 include(":data:model")
+include(":data:database")
 include(":data:local")
 include(":data:remote")
 include(":data:repo")
@@ -38,4 +39,4 @@ include(":feat:design")
 include(":feat:home")
 include(":feat:baby")
 include(":feat:settings")
-include(":data:database")
+include(":feat:webview")