Ver código fonte

add(study plan): add total page

zhaoyadi 1 ano atrás
pai
commit
ab395fe887
100 arquivos alterados com 2684 adições e 526 exclusões
  1. 4 1
      app/src/main/AndroidManifest.xml
  2. 19 14
      app/src/main/java/com/zaojiao/app/MainActivity.kt
  3. 2 0
      core/http/build.gradle.kts
  4. 13 2
      core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetResult.kt
  5. 1 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/di/HttpModule.kt
  6. 16 0
      core/json/src/main/java/com/zaojiao/app/core/json/adapter/ResultAdapter.kt
  7. 6 0
      data/model/src/main/kotlin/com/zaojiao/app/data/model/common/MediaCategoryModel.kt
  8. 6 0
      data/model/src/main/kotlin/com/zaojiao/app/data/model/common/SortCategoryModel.kt
  9. 11 0
      data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanCourseItemModel.kt
  10. 9 0
      data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanCoursePageRequest.kt
  11. 19 0
      data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanHistoryModel.kt
  12. 3 1
      data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanTomorrowModel.kt
  13. 3 0
      data/remote/build.gradle.kts
  14. 59 0
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteCourseData.kt
  15. 4 0
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteStudyPlanData.kt
  16. 29 1
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/api/CourseApi.kt
  17. 5 0
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/api/StudyPlanApi.kt
  18. 45 0
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/paging/BasePagingSource.kt
  19. 3 0
      data/repo/build.gradle.kts
  20. 29 1
      data/repo/src/main/kotlin/com/zaojiao/app/data/repo/StudyPlanRepository.kt
  21. 53 2
      data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/StudyPlanRepositoryImpl.kt
  22. 1 0
      feat/design/build.gradle.kts
  23. 214 18
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/EmptyState.kt
  24. 122 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Paging.kt
  25. 1 1
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/StatePage.kt
  26. 148 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/TabBar.kt
  27. 3 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/viewmodel/BaseViewModel.kt
  28. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_activity.png
  29. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_cart.png
  30. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_collection.png
  31. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_comment.png
  32. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_content.png
  33. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_coupon.png
  34. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_data.png
  35. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_follow.png
  36. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_function.png
  37. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_learning.png
  38. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_message.png
  39. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_network.png
  40. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_notification.png
  41. BIN
      feat/design/src/main/res/mipmap-xhdpi/state_no_order.png
  42. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_activity.png
  43. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_cart.png
  44. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_collection.png
  45. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_comment.png
  46. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_content.png
  47. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_coupon.png
  48. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_data.png
  49. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_follow.png
  50. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_function.png
  51. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_learning.png
  52. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_message.png
  53. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_network.png
  54. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_notification.png
  55. BIN
      feat/design/src/main/res/mipmap-xxhdpi/state_no_order.png
  56. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_activity.png
  57. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_cart.png
  58. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_collection.png
  59. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_comment.png
  60. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_content.png
  61. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_coupon.png
  62. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_data.png
  63. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_follow.png
  64. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_function.png
  65. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_learning.png
  66. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_message.png
  67. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_network.png
  68. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_notification.png
  69. BIN
      feat/design/src/main/res/mipmap-xxxhdpi/state_no_order.png
  70. 4 0
      feat/home/build.gradle.kts
  71. 2 1
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/course/HomeCoursePage.kt
  72. 3 3
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/index/HomeIndexPage.kt
  73. 0 378
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanIndexPage.kt
  74. 0 33
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanIndexViewModel.kt
  75. 52 23
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt
  76. 0 19
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanTotalPage.kt
  77. 0 22
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanViewModel.kt
  78. 284 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/demo/HomePlanDemoPage.kt
  79. 199 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexHistory.kt
  80. 637 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexPage.kt
  81. 84 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexViewModel.kt
  82. 7 3
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/state/HomePlanIndexUiState.kt
  83. 2 3
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/state/HomePlanTotalUiState.kt
  84. 261 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/total/HomePlanTotalPage.kt
  85. 105 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/total/HomePlanTotalViewModel.kt
  86. 216 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/widget/HomePlanTabBar.kt
  87. BIN
      feat/home/src/main/res/mipmap-xhdpi/study_plan_light_off.png
  88. BIN
      feat/home/src/main/res/mipmap-xhdpi/study_plan_light_on.png
  89. BIN
      feat/home/src/main/res/mipmap-xhdpi/study_plan_lock.png
  90. BIN
      feat/home/src/main/res/mipmap-xhdpi/study_plan_lock_white.png
  91. BIN
      feat/home/src/main/res/mipmap-xhdpi/study_plan_no_item.png
  92. BIN
      feat/home/src/main/res/mipmap-xxhdpi/study_plan_light_off.png
  93. BIN
      feat/home/src/main/res/mipmap-xxhdpi/study_plan_light_on.png
  94. BIN
      feat/home/src/main/res/mipmap-xxhdpi/study_plan_lock.png
  95. BIN
      feat/home/src/main/res/mipmap-xxhdpi/study_plan_lock_white.png
  96. BIN
      feat/home/src/main/res/mipmap-xxhdpi/study_plan_no_item.png
  97. BIN
      feat/home/src/main/res/mipmap-xxxhdpi/study_plan_light_off.png
  98. BIN
      feat/home/src/main/res/mipmap-xxxhdpi/study_plan_light_on.png
  99. BIN
      feat/home/src/main/res/mipmap-xxxhdpi/study_plan_lock.png
  100. BIN
      feat/home/src/main/res/mipmap-xxxhdpi/study_plan_lock_white.png

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

@@ -49,7 +49,10 @@
                     android:host="www.luojigou.vip"
                     android:scheme="route" />
             </intent-filter>
+
+            <intent-filter>
+                <data android:scheme="ljg" />
+            </intent-filter>
         </activity>
     </application>
-
 </manifest>

+ 19 - 14
app/src/main/java/com/zaojiao/app/MainActivity.kt

@@ -1,6 +1,7 @@
 package com.zaojiao.app
 
 import android.content.Intent
+import android.net.TrafficStats
 import android.os.BatteryManager
 import android.os.Build
 import android.os.Bundle
@@ -17,6 +18,9 @@ import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import com.google.accompanist.systemuicontroller.rememberSystemUiController
 import com.zaojiao.app.core.auth.data.AuthRepository
 import com.zaojiao.app.core.auth.model.TokenModel
@@ -28,11 +32,7 @@ import com.zaojiao.app.feat.design.LocalBackgroundTheme
 import com.zaojiao.app.feat.design.LocalOnFinishDispatcher
 import com.zaojiao.app.ui.App
 import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.ConcurrentHashMap
@@ -44,7 +44,7 @@ private val LightBackgroundTheme = BackgroundTheme(color = Color.White)
 private val DarkBackgroundTheme = BackgroundTheme(color = Color.Black)
 
 @AndroidEntryPoint
-class MainActivity : FragmentActivity(), ITokenRequest, CoroutineScope by MainScope() {
+class MainActivity : FragmentActivity(), ITokenRequest {
     companion object {
         private const val tokenFlag = 99
         private val requests = ConcurrentHashMap<Int, CompletableFuture<TokenModel?>>()
@@ -64,11 +64,13 @@ class MainActivity : FragmentActivity(), ITokenRequest, CoroutineScope by MainSc
 
             if (currentTimeMillis - lastTimeMillis > 2000) {
                 Toast.makeText(this@MainActivity, "再次点击退出应用", Toast.LENGTH_SHORT).show()
-                launch {
-                    isEnabled = false
-                    delay(2000)
-                    lastTimeMillis = currentTimeMillis
-                    isEnabled = true
+                lifecycleScope.launch {
+                    repeatOnLifecycle(Lifecycle.State.CREATED) {
+                        isEnabled = false
+                        delay(2000)
+                        lastTimeMillis = currentTimeMillis
+                        isEnabled = true
+                    }
                 }
             }
         }
@@ -79,12 +81,16 @@ class MainActivity : FragmentActivity(), ITokenRequest, CoroutineScope by MainSc
         super.onCreate(savedInstanceState)
         configureSystemBar()
 
+        TrafficStats.getMobileRxBytes()
+
         getSystemService(BatteryManager::class.java)
         onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
 
-        launch {
-            authRepository.state.collect {
-                Log.e("MainActivity", "onCreate: $it")
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                authRepository.state.collect {
+                    Log.e("MainActivity", "onCreate: $it")
+                }
             }
         }
 
@@ -124,7 +130,6 @@ class MainActivity : FragmentActivity(), ITokenRequest, CoroutineScope by MainSc
 
     override fun onDestroy() {
         AuthUtils.detach()
-        this.cancel()
         super.onDestroy()
     }
 

+ 2 - 0
core/http/build.gradle.kts

@@ -15,6 +15,8 @@ dependencies {
     implementation(libs.coil.kt)
     implementation(libs.coil.kt.svg)
 
+    implementation("org.jetbrains.kotlin:kotlin-reflect:${libs.versions.kotlin.get()}")
+
     api("com.squareup.okhttp3:okhttp:4.10.0")
     api("com.squareup.okhttp3:logging-interceptor:4.10.0")
 

+ 13 - 2
core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetResult.kt

@@ -7,10 +7,21 @@ import com.zaojiao.app.core.common.state.DataState
 data class NetResult<out T>(
     val status: Int,
     val data: T?,
-    @Json(name = "msg")
-    val message: String?,
+    @Json(name = "msg") val message: String?,
 )
 
+data class NetPage<out T, out S>(
+    val total: Int,
+    val currentPage: Int,
+    val pageSize: Int,
+    val entityList: List<T>,
+    val assistData: S?,
+)
+
+typealias NetPageResult<T> = NetResult<NetPage<T, Any>>
+
+typealias NetPageResultWithAssist<T, S> = NetResult<NetPage<T, S>>
+
 fun NetResult<*>.isSuccess(): Boolean {
     return status == 200
 }

+ 1 - 0
core/http/src/main/kotlin/com/zaojiao/app/core/http/di/HttpModule.kt

@@ -12,6 +12,7 @@ import com.zaojiao.app.core.http.converter.ResultConverterFactory
 import com.zaojiao.app.core.http.interceptor.AuthInterceptor
 import com.zaojiao.app.core.http.interceptor.ResultInterceptor
 import com.zaojiao.app.core.http.interceptor.VersionInterceptor
+import com.zaojiao.app.core.json.adapter.ResultAdapterFactory
 import dagger.Module
 import dagger.Provides
 import dagger.hilt.InstallIn

+ 16 - 0
core/json/src/main/java/com/zaojiao/app/core/json/adapter/ResultAdapter.kt

@@ -0,0 +1,16 @@
+package com.zaojiao.app.core.json.adapter
+
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.rawType
+import java.lang.reflect.Type
+
+class ResultAdapterFactory : JsonAdapter.Factory {
+    override fun create(
+        type: Type, annotations: Set<Annotation>, moshi: Moshi
+    ): JsonAdapter<*>? {
+        val rawType: Class<*> = type.rawType
+        val delegate: JsonAdapter<Any> = moshi.nextAdapter(this, type, annotations)
+        return delegate.serializeNulls()
+    }
+}

+ 6 - 0
data/model/src/main/kotlin/com/zaojiao/app/data/model/common/MediaCategoryModel.kt

@@ -0,0 +1,6 @@
+package com.zaojiao.app.data.model.common
+
+data class MediaCategoryModel(
+    val mediaType: String?,
+    val typeName: String
+)

+ 6 - 0
data/model/src/main/kotlin/com/zaojiao/app/data/model/common/SortCategoryModel.kt

@@ -0,0 +1,6 @@
+package com.zaojiao.app.data.model.common
+
+data class SortCategoryModel(
+    val sortType: String,
+    val typeName: String,
+)

+ 11 - 0
data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanCourseItemModel.kt

@@ -0,0 +1,11 @@
+package com.zaojiao.app.data.model.studyplan
+
+data class StudyPlanCourseItemModel(
+    val completeItemQuantity: Int,
+    val courseImgCover: String?,
+    val courseImgCoverMini: String?,
+    val courseName: String,
+    val courseSkuId: String,
+    val courseSpuId: String,
+    val totalItemQuantity: Int
+)

+ 9 - 0
data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanCoursePageRequest.kt

@@ -0,0 +1,9 @@
+package com.zaojiao.app.data.model.studyplan
+
+data class StudyPlanCoursePageRequest(
+    val page: Int,
+    val pageSize: Int = 10,
+    val mediaType: String?,
+    val sortType: String,
+    val categoryId: String,
+)

+ 19 - 0
data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanHistoryModel.kt

@@ -0,0 +1,19 @@
+package com.zaojiao.app.data.model.studyplan
+
+data class StudyPlanHistoryModel(
+    val completeTaskNum: Int,
+    val courseCategoryName: String,
+    val courseItemId: String,
+    val courseItemImgCover: String,
+    val courseItemName: String,
+    val courseName: String,
+    val courseSkuId: String,
+    val courseSpuId: String,
+    val createTime: String,
+    val gameCourseId: String,
+    val totalTask: Int,
+    val unlockDate: String,
+    val unlockState: Int,
+    val unlockTime: String,
+    val unlockWeek: String
+)

+ 3 - 1
data/model/src/main/kotlin/com/zaojiao/app/data/model/studyplan/StudyPlanTomorrowModel.kt

@@ -1,5 +1,7 @@
 package com.zaojiao.app.data.model.studyplan
 
+import com.zaojiao.app.core.json.adapter.StateInt
+
 data class StudyPlanTomorrowModel(
     val courseItemId: String,
     val courseItemImgCover: String,
@@ -7,6 +9,6 @@ data class StudyPlanTomorrowModel(
     val courseName: String,
     val courseSkuId: String,
     val courseSpuId: String,
-    val tomorrowUnlock: Int,
+    @StateInt val tomorrowUnlock: Boolean,
     val unlockDate: String,
 )

+ 3 - 0
data/remote/build.gradle.kts

@@ -12,4 +12,7 @@ dependencies {
     implementation(project(":core:http"))
 
     implementation(project(":data:model"))
+
+    implementation("androidx.paging:paging-runtime-ktx:3.2.0")
+    implementation("androidx.paging:paging-compose:3.2.0")
 }

+ 59 - 0
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteCourseData.kt

@@ -0,0 +1,59 @@
+package com.zaojiao.app.data.remote
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.zaojiao.app.core.http.common.NetPageResult
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCoursePageRequest
+import com.zaojiao.app.data.remote.api.CourseApi
+import com.zaojiao.app.data.remote.paging.BasingPagingResource
+import kotlinx.coroutines.flow.Flow
+import retrofit2.Retrofit
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RemoteCourseData @Inject constructor(
+    private val retrofit: Retrofit,
+) {
+    private val courseApi = retrofit.create(CourseApi::class.java)
+
+    suspend fun getAllTypeCategory() = courseApi.getAllTypeCategory().data
+
+    suspend fun getAllMediaCategory() = courseApi.getAllMediaCategory().data
+
+    /** 学习计划 - 全部课程页面的 ‘分类’ 列表 **/
+    suspend fun getAllMediaCategoryV2() = courseApi.getAllMediaCategoryV2().data
+
+    suspend fun getCourseByCategory(
+        mediaType: String?,
+        sortType: String,
+        courseType: String,
+    ): Flow<PagingData<StudyPlanCourseItemModel>> {
+        return Pager(
+            config = PagingConfig(
+                pageSize = 10,
+                enablePlaceholders = false,
+            ),
+            pagingSourceFactory = {
+                StudyPlanCoursePagingSource(
+                    courseApi, mediaType, sortType, courseType
+                )
+            },
+        ).flow
+    }
+
+    class StudyPlanCoursePagingSource(
+        private val courseApi: CourseApi,
+        private val mediaType: String?,
+        private val sortType: String,
+        private val courseType: String,
+    ) : BasingPagingResource<StudyPlanCourseItemModel>() {
+        override suspend fun getData(page: Int): NetPageResult<StudyPlanCourseItemModel> {
+            return courseApi.getCourseByCategory(
+                StudyPlanCoursePageRequest(page, 10, mediaType, sortType, courseType)
+            )
+        }
+    }
+}

+ 4 - 0
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteStudyPlanData.kt

@@ -1,5 +1,6 @@
 package com.zaojiao.app.data.remote
 
+import com.zaojiao.app.data.model.common.CategoryModel
 import com.zaojiao.app.data.remote.api.StudyPlanApi
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -13,4 +14,7 @@ class RemoteStudyPlanData @Inject constructor(
     suspend fun getTomorrowPlan() = studyPlanApi.getTomorrowPlan().data
 
     suspend fun getTotalCategory() = studyPlanApi.getTotalCategory().data
+
+    suspend fun getHistoryPlan(categoryModel: CategoryModel) =
+        studyPlanApi.getHistoryPlan(categoryModel.id).data
 }

+ 29 - 1
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/api/CourseApi.kt

@@ -1,4 +1,32 @@
 package com.zaojiao.app.data.remote.api
 
-class CourseApi {
+import com.zaojiao.app.core.http.common.NetPageResult
+import com.zaojiao.app.core.http.common.NetResult
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.common.MediaCategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCoursePageRequest
+import retrofit2.http.Body
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.GET
+import retrofit2.http.POST
+
+interface CourseApi {
+    @GET("/app/app/course/media/list")
+    suspend fun getAllMediaCategory(): NetResult<List<MediaCategoryModel>>
+
+    @GET("/app/app/course/media/list/v2")
+    suspend fun getAllMediaCategoryV2(): NetResult<List<MediaCategoryModel>>
+
+    @GET("/app/app/course/categoryAll")
+    suspend fun getAllTypeCategory(): NetResult<List<CategoryModel>>
+
+    @GET("/app/app/course/age/list")
+    suspend fun getAllAgeCategory(): NetResult<List<String>>
+
+    @POST("/app/ai/course/page")
+    suspend fun getCourseByCategory(
+        @Body request: StudyPlanCoursePageRequest,
+    ): NetPageResult<StudyPlanCourseItemModel>
 }

+ 5 - 0
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/api/StudyPlanApi.kt

@@ -2,9 +2,11 @@ package com.zaojiao.app.data.remote.api
 
 import com.zaojiao.app.core.http.common.NetResult
 import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
 import retrofit2.http.GET
+import retrofit2.http.Query
 
 interface StudyPlanApi {
     @GET("/app/game-course/plan/today")
@@ -15,4 +17,7 @@ interface StudyPlanApi {
 
     @GET("/app/game-course/plan/category")
     suspend fun getTotalCategory(): NetResult<List<CategoryModel>>
+
+    @GET("/app/game-course/plan/history")
+    suspend fun getHistoryPlan(@Query("categoryId") categoryId: String): NetResult<List<StudyPlanHistoryModel>>
 }

+ 45 - 0
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/paging/BasePagingSource.kt

@@ -0,0 +1,45 @@
+package com.zaojiao.app.data.remote.paging
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.zaojiao.app.core.http.common.NetPageResult
+import com.zaojiao.app.core.http.common.isSuccess
+
+abstract class BasingPagingResource<T : Any> : PagingSource<Int, T>() {
+    override val keyReuseSupported: Boolean
+        get() = true
+
+    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
+        return state.anchorPosition?.let { anchorPosition ->
+            val anchorPage = state.closestPageToPosition(anchorPosition)
+            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
+        }
+    }
+
+    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
+        val requestPage = params.key ?: 1
+
+        return try {
+            val response = getData(requestPage)
+
+            with(response) {
+                if (!isSuccess() || data == null) {
+                    LoadResult.Invalid()
+                } else {
+                    data!!.run {
+                        LoadResult.Page(
+                            data = entityList,
+                            prevKey = if (currentPage <= 1) null else currentPage - 1,
+                            nextKey = if (currentPage >= total) null else currentPage + 1,
+                        )
+                    }
+                }
+            }
+        } catch (e: Throwable) {
+            e.printStackTrace()
+            LoadResult.Error(e)
+        }
+    }
+
+    abstract suspend fun getData(page: Int): NetPageResult<T>
+}

+ 3 - 0
data/repo/build.gradle.kts

@@ -14,4 +14,7 @@ dependencies {
     implementation(project(":data:remote"))
     implementation(project(":data:local"))
     implementation(project(":data:model"))
+
+    api("androidx.paging:paging-runtime-ktx:3.2.0")
+    api("androidx.paging:paging-compose:3.2.0")
 }

+ 29 - 1
data/repo/src/main/kotlin/com/zaojiao/app/data/repo/StudyPlanRepository.kt

@@ -1,12 +1,40 @@
 package com.zaojiao.app.data.repo
 
+import androidx.paging.PagingData
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.common.MediaCategoryModel
+import com.zaojiao.app.data.model.common.SortCategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
+import kotlinx.coroutines.flow.Flow
 
 interface StudyPlanRepository {
-    suspend fun getHistoryList():List<StudyPlanTodayModel>
 
+    /**
+     * 学习计划 -> 个人页面
+     */
     suspend fun getTodayPlanList(): List<StudyPlanTodayModel>
 
     suspend fun getTomorrowPlanList(): List<StudyPlanTomorrowModel>
+
+    suspend fun getHistoryCategory(): List<CategoryModel>
+
+    suspend fun getHistoryPlanList(categoryModel: CategoryModel): List<StudyPlanHistoryModel>
+
+    /**
+     * 学习计划 -> 全部课程
+     */
+    suspend fun getCourseMediaCategory(): List<MediaCategoryModel>
+
+    suspend fun getCourseSortCategory(): List<SortCategoryModel>
+
+    suspend fun getCourseTypeCategory(): List<CategoryModel>
+
+    suspend fun getStudyPlanCourse(
+        mediaType: String?,
+        sortType: String,
+        courseType: String,
+    ): Flow<PagingData<StudyPlanCourseItemModel>>
 }

+ 53 - 2
data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/StudyPlanRepositoryImpl.kt

@@ -1,15 +1,25 @@
 package com.zaojiao.app.data.repo.impl
 
+import androidx.paging.PagingData
 import com.zaojiao.app.core.common.remote.di.ApplicationScope
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.common.MediaCategoryModel
+import com.zaojiao.app.data.model.common.SortCategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
+import com.zaojiao.app.data.remote.RemoteCourseData
 import com.zaojiao.app.data.remote.RemoteStudyPlanData
 import com.zaojiao.app.data.repo.StudyPlanRepository
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import java.lang.IllegalArgumentException
 import javax.inject.Inject
 
 class StudyPlanRepositoryImpl @Inject constructor(
     private val remoteStudyPlanData: RemoteStudyPlanData,
+    private val remoteCourseData: RemoteCourseData,
     @ApplicationScope private val coroutineScope: CoroutineScope,
 ) : StudyPlanRepository {
     override suspend fun getTodayPlanList(): List<StudyPlanTodayModel> {
@@ -25,7 +35,48 @@ class StudyPlanRepositoryImpl @Inject constructor(
         }
     }
 
-    override suspend fun getHistoryList(): List<StudyPlanTodayModel> {
-        return emptyList()
+    override suspend fun getHistoryCategory(): List<CategoryModel> {
+        return remoteStudyPlanData.getTotalCategory().run {
+            this ?: emptyList()
+        }
+    }
+
+    override suspend fun getHistoryPlanList(categoryModel: CategoryModel): List<StudyPlanHistoryModel> {
+        return remoteStudyPlanData.getHistoryPlan(categoryModel).run {
+            this ?: emptyList()
+        }
+    }
+
+    override suspend fun getCourseMediaCategory(): List<MediaCategoryModel> {
+        return remoteCourseData.getAllMediaCategoryV2().run {
+            this ?: throw IllegalArgumentException("")
+        }
+    }
+
+    override suspend fun getCourseSortCategory(): List<SortCategoryModel> {
+        return listOf(
+            SortCategoryModel(
+                sortType = "0",
+                typeName = "最近在学"
+            ),
+            SortCategoryModel(
+                sortType = "1",
+                typeName = "最近更新"
+            ),
+        )
+    }
+
+    override suspend fun getCourseTypeCategory(): List<CategoryModel> {
+        return remoteCourseData.getAllTypeCategory().run {
+            this ?: throw IllegalArgumentException("")
+        }
+    }
+
+    override suspend fun getStudyPlanCourse(
+        mediaType: String?,
+        sortType: String,
+        courseType: String,
+    ): Flow<PagingData<StudyPlanCourseItemModel>> {
+        return remoteCourseData.getCourseByCategory(mediaType, sortType, courseType)
     }
 }

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

@@ -12,4 +12,5 @@ dependencies {
     implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
 
     implementation(libs.coil.kt.compose)
+    implementation("androidx.paging:paging-compose:3.2.0")
 }

+ 214 - 18
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/EmptyState.kt

@@ -1,29 +1,47 @@
 package com.zaojiao.app.feat.design
 
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 
 /**
  *  空状态
- *
- *  ```kt
- *     EmptyState.NoMessage()
- *  ```
- *
- *  [NoMessage]: 无消息
- *  [NoFollow]: 无关注
- *  [NoSearchResult]: 无搜索结果
  */
 object EmptyState {
     @Composable
-    fun NoMessage() {
-
+    fun NoMessage(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_message,
+            message = message ?: "现在没有消息哦~",
+        )
     }
 
     /**
      *  无关注
      */
     @Composable
-    fun NoFollow() {
+    fun NoFollow(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_message,
+            message = message ?: "现在没有关注哦~",
+        )
 
     }
 
@@ -31,31 +49,209 @@ object EmptyState {
      *  无搜索结果
      */
     @Composable
-    fun NoSearchResult() {
-
+    fun NoSearchResult(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_content,
+            message = message ?: "没有想要找的内容哦~",
+        )
     }
 
     /**
      *  无活动
      */
     @Composable
-    fun NoActivity() {
-
+    fun NoActivity(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_activity,
+            message = message ?: "现在还没有活动哦~",
+        )
     }
 
     /**
      *  无学习
      */
     @Composable
-    fun NoLearning() {
-
+    fun NoLearning(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_learning,
+            message = message ?: "您现在还没有开始学习哦~",
+        )
     }
 
     /**
      *  无评论
      */
     @Composable
-    fun NoComment() {
+    fun NoComment(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_comment,
+            message = message ?: "目前还没有评论哦~",
+        )
+    }
+
+    /**
+     * 无收藏
+     */
+    @Composable
+    fun NoCollection(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_collection,
+            message = message ?: "暂无收藏",
+        )
+    }
+
+    /**
+     *  无通知
+     */
+    @Composable
+    fun NoNotification(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_notification,
+            message = message ?: "亲~您还没收到通知哦!",
+        )
+    }
+
+    /**
+     *  无优惠券
+     */
+    @Composable
+    fun NoCoupon(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_coupon,
+            message = message ?: "现在没有优惠券哦~",
+        )
+    }
+
+    /**
+     *  无数据
+     */
+    @Composable
+    fun NoData(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_data,
+            message = message ?: "此页面为空",
+        )
+    }
+
+    /**
+     *  无订单
+     */
+    @Composable
+    fun NoOrder(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_order,
+            message = message ?: "订单为空",
+        )
+    }
+
+    /**
+     *  无网络
+     */
+    @Composable
+    fun NoNetwork(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_network,
+            message = message ?: "亲~您的网络不给力哦!",
+        )
+    }
+
+    /**
+     *  购物车为空
+     */
+    @Composable
+    fun NoCart(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_cart,
+            message = message ?: "购物车为空",
+        )
+    }
+
+
+    /**
+     *  无功能
+     */
+    @Composable
+    fun NoFunction(
+        modifier: Modifier = Modifier,
+        message: String? = null,
+    ) {
+        EmptyStateImage(
+            modifier = modifier,
+            id = R.mipmap.state_no_function,
+            message = message ?: "功能正在努力开发中\n敬请期待",
+        )
+    }
+}
 
+@Composable
+private fun EmptyStateImage(
+    @DrawableRes id: Int,
+    message: String,
+    modifier: Modifier = Modifier,
+) {
+    Column(
+        modifier = modifier.wrapContentSize(),
+    ) {
+        Images.Resource(
+            id = id,
+            modifier = Modifier.size(163.dp, 126.dp),
+        )
+        Spacer(height = 20.dp)
+        Text(
+            text = message,
+            style = TextStyle(
+                color = Colors.FF333333,
+                fontSize = 14.sp,
+                lineHeight = 25.sp,
+                fontWeight = FontWeight.Normal,
+            ),
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally)
+                .wrapContentSize(),
+        )
     }
 }

+ 122 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Paging.kt

@@ -0,0 +1,122 @@
+package com.zaojiao.app.feat.design
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.unit.dp
+import androidx.paging.LoadState
+import androidx.paging.compose.LazyPagingItems
+
+@Composable
+inline fun <reified T : Any> LazyPagingItems<T>.onState(
+    content: @Composable (LazyPagingItems<T>) -> Unit,
+) {
+    val isListEmpty = loadState.refresh is LoadState.NotLoading && itemCount == 0
+
+    Column(
+        modifier = Modifier.fillMaxSize(),
+    ) {
+        Box(
+            modifier = Modifier
+                .weight(1f)
+                .fillMaxWidth()
+        ) {
+            when (loadState.source.refresh) {
+                is LoadState.Error -> {
+                    EmptyState.NoNetwork(
+                        modifier = Modifier
+                            .align(Alignment.Center)
+                    )
+
+//                    Text(
+//                        text = "重新加载",
+//                        modifier = Modifier
+//                            .align(Alignment.Center)
+//                            .clickable {
+//                                retry()
+//                            }
+//                            .background(
+//                                color = Color.Red,
+//                                shape = RoundedCornerShape(100),
+//                            )
+//                            .padding(
+//                                horizontal = 20.dp,
+//                                vertical = 8.dp,
+//                            ),
+//                    )
+                }
+
+                LoadState.Loading -> {
+                    Text(
+                        text = "加载中",
+                        modifier = Modifier
+                            .align(Alignment.Center)
+                            .clip(
+                                shape = RoundedCornerShape(100),
+                            )
+                            .background(
+                                color = Color.Blue,
+                            )
+                            .padding(
+                                horizontal = 20.dp,
+                                vertical = 8.dp,
+                            ),
+                    )
+                }
+
+                is LoadState.NotLoading -> {
+                    if (itemCount == 0) {
+                        EmptyState.NoData(
+                            modifier = Modifier
+                                .align(Alignment.Center)
+                        )
+                    } else {
+                        content(this@onState)
+                    }
+                }
+            }
+        }
+
+        when (loadState.source.append) {
+            is LoadState.Error -> {
+                Text(
+                    text = "加载更多",
+                    modifier = Modifier
+                        .align(Alignment.CenterHorizontally)
+                        .clickable {
+                            retry()
+                        }
+                        .padding(
+                            horizontal = 20.dp,
+                            vertical = 8.dp,
+                        ),
+                )
+            }
+
+            LoadState.Loading -> {
+                Text(
+                    text = "加载中",
+                    modifier = Modifier
+                        .align(Alignment.CenterHorizontally)
+                        .padding(
+                            horizontal = 20.dp,
+                            vertical = 8.dp,
+                        ),
+                )
+            }
+
+            is LoadState.NotLoading -> {}
+        }
+    }
+}

+ 1 - 1
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/StatePage.kt

@@ -22,7 +22,7 @@ sealed interface UiState<out T> {
 }
 
 @Composable
-fun <T> StatePage(uiState: UiState<T>, onSuccess: @Composable T.() -> Unit) {
+inline fun <reified T> StatePage(uiState: UiState<T>, onSuccess: @Composable (T) -> Unit) {
     when (uiState) {
         is UiState.Loading -> {
             Box(

+ 148 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/TabBar.kt

@@ -0,0 +1,148 @@
+package com.zaojiao.app.feat.design
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+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.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+@Immutable
+private class TabItemPosition(val left: Dp, val width: Dp) {
+    val right: Dp get() = left + width
+
+    val center: Dp get() = left + width / 2
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TabItemPosition) return false
+
+        if (left != other.left) return false
+        if (width != other.width) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = left.hashCode()
+        result = 31 * result + width.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "TabItemPosition(left=$left, right=$right, width=$width)"
+    }
+}
+
+@Composable
+fun TabBar(
+    modifier: Modifier = Modifier,
+    initial: Int = 0,
+    tabs: List<String>,
+    selectColor: Color,
+    unselectColor: Color,
+    selectStyle: TextStyle,
+    unselectStyle: TextStyle,
+    onChange: ((Int) -> Unit)? = null,
+) {
+    val density = LocalDensity.current.density
+
+    val coroutineScope = rememberCoroutineScope()
+
+    val lazyListState = rememberLazyListState()
+
+    var lazyListWidth by remember { mutableStateOf(0) }
+
+    val itemPositions = remember { MutableList(tabs.size) { TabItemPosition(0.dp, 0.dp) } }
+
+    var selectIndex by remember { mutableStateOf(initial) }
+
+    LazyRow(
+        state = lazyListState,
+        modifier = Modifier
+            .onGloballyPositioned {
+                lazyListWidth = it.size.width
+            }
+            .fillMaxWidth()
+            .wrapContentHeight(),
+    ) {
+        items(tabs.size) { index ->
+            TabBarItem(
+                title = tabs[index],
+                style = if (index == selectIndex) selectStyle else unselectStyle,
+                modifier = Modifier
+                    .padding(horizontal = 8.dp)
+                    .onGloballyPositioned {
+                        val left = it
+                            .positionInParent()
+                            .round().x
+                        val width = it.size.width
+
+                        itemPositions[index] = TabItemPosition(
+                            (left / density).roundToInt().dp,
+                            (width / density).roundToInt().dp,
+                        )
+                    }
+                    .clip(shape = RoundedCornerShape(100.dp))
+                    .clickable {
+                        coroutineScope.launch {
+                            if (selectIndex == index) return@launch
+                            selectIndex = index
+                            onChange?.invoke(index)
+                            lazyListState.animateScrollToItem(
+                                index,
+                                (-lazyListWidth / 2 + itemPositions[index].width.value * density / 2).roundToInt()
+                            )
+                        }
+                    }
+                    .background(color = if (index == selectIndex) selectColor else unselectColor)
+                    .widthIn(min = 73.dp)
+                    .wrapContentSize(),
+            )
+        }
+    }
+}
+
+@Composable
+private fun TabBarItem(
+    title: String,
+    modifier: Modifier = Modifier,
+    style: TextStyle,
+) {
+    Box(modifier = modifier) {
+        Text(
+            text = title,
+            style = style,
+            modifier = Modifier
+                .align(Alignment.Center)
+                .padding(5.dp, 7.dp, 3.dp, 10.dp)
+                .wrapContentSize(),
+        )
+    }
+}

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

@@ -1,6 +1,9 @@
 package com.zaojiao.app.feat.design.viewmodel
 
 import android.util.Log
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.zaojiao.app.feat.design.UiState

BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_activity.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_cart.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_collection.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_comment.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_content.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_coupon.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_data.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_follow.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_function.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_learning.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_message.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_network.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_notification.png


BIN
feat/design/src/main/res/mipmap-xhdpi/state_no_order.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_activity.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_cart.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_collection.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_comment.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_content.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_coupon.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_data.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_follow.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_function.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_learning.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_message.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_network.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_notification.png


BIN
feat/design/src/main/res/mipmap-xxhdpi/state_no_order.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_activity.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_cart.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_collection.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_comment.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_content.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_coupon.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_data.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_follow.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_function.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_learning.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_message.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_network.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_notification.png


BIN
feat/design/src/main/res/mipmap-xxxhdpi/state_no_order.png


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

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

+ 2 - 1
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/course/HomeCoursePage.kt

@@ -19,6 +19,7 @@ import com.zaojiao.app.feat.design.grid
 internal fun HomeCourseRoute(
     viewModel: HomeCourseViewModel = hiltViewModel(),
 ) {
+
     HomeCoursePage()
 }
 
@@ -31,7 +32,7 @@ fun HomeCoursePage() {
     ) {
         val width = Screen.width()
 
-        StatePage(uiState = UiState.Loading) {
+        StatePage(uiState = UiState.Success(0)) { state ->
             LazyColumn(
                 modifier = Modifier.fillMaxWidth()
             ) {

+ 3 - 3
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/index/HomeIndexPage.kt

@@ -28,10 +28,10 @@ internal fun HomeIndexRoute(
     val babyUiState by viewModel.babyUiState.collectAsStateWithLifecycle()
     val indexUiState by viewModel.stateFlow.collectAsStateWithLifecycle()
 
-    StatePage(uiState = indexUiState) {
+    StatePage(uiState = indexUiState) { state ->
         HomeIndexPage(
             babyUiState = babyUiState,
-            indexUiState = this,
+            indexUiState = state,
             onBabyInfoClick = { }
         )
     }
@@ -70,7 +70,7 @@ fun HomeIndexPage(
         item {
             HomeIndexSwiper2(
                 list = indexUiState.bannerList,
-                )
+            )
         }
 
         item {

+ 0 - 378
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanIndexPage.kt

@@ -1,378 +0,0 @@
-package com.zaojiao.app.feat.home.plan
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-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.aspectRatio
-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.width
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.PathEffect
-import androidx.compose.ui.graphics.drawscope.DrawStyle
-import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
-import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
-import com.zaojiao.app.feat.design.Colors
-import com.zaojiao.app.feat.design.Expanded
-import com.zaojiao.app.feat.design.Images
-import com.zaojiao.app.feat.design.Spacer
-import com.zaojiao.app.feat.design.StatePage
-
-@Composable
-internal fun HomePlanIndexPage(
-    viewModel: HomePlanIndexViewModel = hiltViewModel()
-) {
-    val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
-
-    StatePage(uiState = uiState) {
-        LazyColumn(
-            modifier = Modifier.fillMaxWidth()
-        ) {
-            item {
-                HomePlanTomorrow(
-                    tomorrowPlanList = this@StatePage.tomorrowPlanList,
-                )
-            }
-
-            item {
-                Box(modifier = Modifier.height(20.dp))
-            }
-
-            item {
-                HomePlanToday(
-                    todayPlanList = this@StatePage.todayPlanList,
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-internal fun HomePlanTomorrow(
-    tomorrowPlanList: List<StudyPlanTomorrowModel>
-) {
-    val count = tomorrowPlanList.size
-    val pagerState = rememberPagerState()
-
-    HorizontalPager(
-        pageCount = count,
-        state = pagerState,
-        modifier = Modifier
-            .padding(horizontal = 16.dp)
-            .clip(RoundedCornerShape(35.dp))
-            .fillMaxWidth()
-            .height(70.dp)
-    ) {
-        val plan = tomorrowPlanList[it]
-
-        Box(
-            modifier = Modifier
-                .clip(RoundedCornerShape(35.dp))
-                .fillMaxSize(),
-        ) {
-            Images.Color(
-                color = Colors.from("#FFFF8024"),
-                description = "",
-                modifier = Modifier.fillMaxSize(),
-            )
-
-            Row(modifier = Modifier.fillMaxSize()) {
-                Images.Network(
-                    url = plan.courseItemImgCover,
-                    modifier = Modifier
-                        .padding(6.dp)
-                        .border(
-                            width = 1.dp,
-                            color = Color.White,
-                            shape = RoundedCornerShape(35.dp),
-                        )
-                        .clip(RoundedCornerShape(35.dp))
-                        .background(color = Colors.from("#36000000"))
-                        .aspectRatio(ratio = 1f),
-                )
-            }
-        }
-    }
-
-    if (count > 1) {
-        Row(
-            modifier = Modifier
-                .padding(top = 8.dp)
-                .fillMaxWidth()
-                .height(4.dp),
-            horizontalArrangement = Arrangement.Center,
-        ) {
-            repeat(count) {
-                if (it == pagerState.currentPage) {
-                    Box(
-                        modifier = Modifier
-                            .padding(horizontal = 2.dp)
-                            .background(
-                                color = Colors.from("#d8d8d8"),
-                                shape = RoundedCornerShape(2.dp),
-                            )
-                            .fillMaxHeight()
-                            .width(16.dp),
-                    )
-                } else {
-                    Box(
-                        modifier = Modifier
-                            .padding(horizontal = 2.dp)
-                            .background(
-                                color = Colors.from("#d8d8d8"),
-                                shape = RoundedCornerShape(2.dp),
-                            )
-                            .height(4.dp)
-                            .width(4.dp),
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-internal fun HomePlanToday(
-    todayPlanList: List<StudyPlanTodayModel>
-) {
-    Box(
-        modifier = Modifier
-            .padding(horizontal = 16.dp)
-            .fillMaxWidth()
-            .height(227.dp),
-    ) {
-        Box(
-            modifier = Modifier
-                .background(
-                    brush = Brush.horizontalGradient(
-                        colors = listOf(
-                            Colors.from("#EBFEE6D5"),
-                            Colors.from("#FFFFE7D6"),
-                        )
-                    ),
-                    shape = RoundedCornerShape(20.dp)
-                )
-                .fillMaxSize()
-        )
-
-        Column(
-            modifier = Modifier.fillMaxSize(),
-        ) {
-            Row(
-                modifier = Modifier
-                    .padding(horizontal = 12.dp, vertical = 20.dp)
-                    .fillMaxWidth()
-                    .wrapContentHeight(),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-                Text(
-                    text = "今日计划",
-                    style = TextStyle(
-                        color = Colors.from("#FFFF8024"),
-                        fontWeight = FontWeight.SemiBold,
-                        fontSize = 20.sp,
-                        lineHeight = 28.sp,
-                    ),
-                )
-
-                Spacer(width = 6.dp)
-
-                Text(
-                    text = "共${todayPlanList.size}节",
-                    style = TextStyle(
-                        color = Colors.from("#FFFF8024"),
-                        fontWeight = FontWeight.Medium,
-                        fontSize = 11.sp,
-                        lineHeight = 12.sp,
-                    ),
-                    modifier = Modifier
-                        .background(
-                            color = Colors
-                                .from("#FFFF8024")
-                                .copy(alpha = 0.2f),
-                            shape = RoundedCornerShape(
-                                topStart = 15.dp,
-                                bottomStart = 2.dp,
-                                topEnd = 15.dp,
-                                bottomEnd = 15.dp,
-                            )
-                        )
-                        .padding(horizontal = 7.dp, vertical = 3.dp)
-                        .wrapContentSize(),
-                )
-
-                Expanded()
-                Text(
-                    text = "计划日历",
-                    style = TextStyle(
-                        color = Colors.from("#FFFF8024"),
-                        fontWeight = FontWeight.Normal,
-                        fontSize = 14.sp,
-                        lineHeight = 16.sp,
-                    ),
-                )
-            }
-
-            LazyRow(
-                modifier = Modifier
-                    .weight(1f)
-                    .fillMaxWidth(),
-            ) {
-                item {
-                    Box(modifier = Modifier.width(12.dp))
-                }
-                todayPlanList.forEach {
-                    item {
-                        HomePlanTodayItem(it)
-                    }
-                }
-                item {
-                    Box(modifier = Modifier.width(12.dp))
-                }
-            }
-
-            Spacer(height = 20.dp)
-        }
-    }
-}
-
-@Composable
-private fun HomePlanTodayItem(today: StudyPlanTodayModel) {
-    Box(
-        modifier = Modifier
-            .background(color = Color.Red.copy(alpha = 0.3f))
-            .fillMaxHeight()
-            .width(262.dp),
-    ) {
-        Box(
-            modifier = Modifier
-                .padding(start = 9.dp, bottom = 9.dp)
-                .align(Alignment.TopStart)
-                .border(
-                    width = 3.dp, color = Color.White,
-                    shape = RoundedCornerShape(14.dp)
-                )
-                .background(
-                    color = Color.Green.copy(alpha = 0.3f),
-                    shape = RoundedCornerShape(14.dp),
-                )
-                .fillMaxHeight()
-                .width(96.dp)
-        ) {
-            Images.Network(
-                url = today.courseItemImgCover,
-                modifier = Modifier
-                    .clip(RoundedCornerShape(14.dp))
-                    .fillMaxSize(),
-            )
-
-            Text(
-                text = today.courseCategoryName,
-                style = TextStyle(
-                    color = Color.White,
-                    fontWeight = FontWeight.SemiBold,
-                    fontSize = 10.sp,
-                    lineHeight = 10.sp,
-                ),
-                modifier = Modifier
-                    .align(Alignment.TopStart)
-                    .padding(3.dp)
-                    .drawWithContent {
-                        this.drawContent()
-                        val radius = 3.dp.toPx()
-                        val radius2 = 6.dp.toPx()
-
-                        val width = this.size.width
-                        val height = this.size.height
-
-                        val path = Path().apply {
-                            moveTo(width + radius, 0f)
-                            arcTo(
-                                rect = Rect(
-                                    center = Offset(width + radius, radius),
-                                    radius = radius,
-                                ),
-                                startAngleDegrees = -100f,
-                                sweepAngleDegrees = -90f,
-                                forceMoveTo = true,
-                            )
-                            lineTo(width, height - radius)
-                            arcTo(
-                                rect = Rect(
-                                    center = Offset(width - radius2, height - radius2),
-                                    radius = radius2,
-                                ),
-                                startAngleDegrees = 0f,
-                                sweepAngleDegrees = 80f,
-                                forceMoveTo = true,
-                            )
-                            lineTo(radius, height)
-                            arcTo(
-                                rect = Rect(
-                                    center = Offset(radius, height + radius),
-                                    radius = radius,
-                                ),
-                                startAngleDegrees = -90f,
-                                sweepAngleDegrees = -90f,
-                                forceMoveTo = true,
-                            )
-                        }
-
-                        this.drawPath(
-                            path = path,
-                            color = Color.White,
-                            style = Stroke(
-                                width = 3.dp.toPx(),
-                            )
-                        )
-                    }
-                    .background(
-                        color = Colors
-                            .from("#FFFF8024"),
-                        shape = RoundedCornerShape(
-                            topStart = 10.dp,
-                            bottomStart = 4.dp,
-                            topEnd = 0.dp,
-                            bottomEnd = 0.dp,
-                        )
-                    )
-                    .padding(horizontal = 7.dp, vertical = 3.dp)
-                    .wrapContentSize(),
-            )
-        }
-    }
-}

+ 0 - 33
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanIndexViewModel.kt

@@ -1,33 +0,0 @@
-package com.zaojiao.app.feat.home.plan
-
-import androidx.lifecycle.viewModelScope
-import com.zaojiao.app.core.auth.data.AuthRepository
-import com.zaojiao.app.core.auth.utils.AuthState
-import com.zaojiao.app.data.repo.StudyPlanRepository
-import com.zaojiao.app.feat.design.viewmodel.BaseViewModel
-import com.zaojiao.app.feat.home.plan.state.HomePlanIndexUiState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-class HomePlanIndexViewModel @Inject constructor(
-    private val authRepository: AuthRepository,
-    private val studyPlanRepository: StudyPlanRepository,
-) : BaseViewModel<HomePlanIndexUiState>() {
-
-    init {
-        initialState()
-    }
-
-    override suspend fun productState(): HomePlanIndexUiState {
-        val todayPlanList = studyPlanRepository.getTodayPlanList()
-        val tomorrowPlanList = studyPlanRepository.getTomorrowPlanList()
-
-        return HomePlanIndexUiState(
-            todayPlanList = todayPlanList,
-            tomorrowPlanList = tomorrowPlanList,
-        )
-    }
-}

+ 52 - 23
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt

@@ -1,22 +1,30 @@
 package com.zaojiao.app.feat.home.plan
 
-import android.util.Log
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.statusBarsPadding
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
-import androidx.compose.material3.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.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
-import androidx.navigation.NavBackStackEntry
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+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.feat.design.Colors
+import com.zaojiao.app.feat.home.plan.index.HomePlanIndexPage
+import com.zaojiao.app.feat.home.plan.total.HomePlanTotalPage
+import com.zaojiao.app.feat.home.plan.widget.HomePlanTabBar
 import kotlinx.coroutines.launch
 
 @Composable
@@ -31,27 +39,48 @@ fun HomePlanPage() {
     val pageState = rememberPagerState()
     val coroutineScope = rememberCoroutineScope()
 
-    val current = LocalViewModelStoreOwner.current
-    Log.e("view-model", "HomePlanPage: ${current is NavBackStackEntry}")
+    var tabPosition by remember { mutableStateOf(0f) }
 
     Column(
         modifier = Modifier
+            .background(color = Colors.from("#FFF9F9F9"))
+            .background(
+                brush = Brush.verticalGradient(
+                    colors = listOf(
+                        Colors.from("#7FFF8024"),
+                        Colors.from("#00FF8024"),
+                    ),
+                    startY = 0f,
+                    endY = tabPosition,
+                )
+            )
             .statusBarsPadding()
             .fillMaxSize()
     ) {
-        TabRow(selectedTabIndex = pageState.currentPage) {
-            tabTitles.forEachIndexed { index, title ->
-                Tab(selected = pageState.currentPage == index,
-                    onClick = {
-                        coroutineScope.launch {
-                            pageState.scrollToPage(index)
-
-                        }
-                    }) {
-                    Text(text = title)
+        HomePlanTabBar(
+            initial = pageState.currentPage,
+            tabs = tabTitles,
+            selectStyle = TextStyle(
+                fontSize = 24.sp,
+                lineHeight = 28.sp,
+                fontWeight = FontWeight.SemiBold,
+                color = Color.Black,
+            ),
+            unselectStyle = TextStyle(
+                fontSize = 20.sp,
+                lineHeight = 28.sp,
+                fontWeight = FontWeight.Normal,
+                color = Colors.FF999999,
+            ),
+            onChange = {
+                coroutineScope.launch {
+                    pageState.scrollToPage(it)
                 }
-            }
-        }
+            },
+            modifier = Modifier.onGloballyPositioned {
+                tabPosition = it.positionInParent().y + it.size.height.toFloat()
+            },
+        )
 
         HorizontalPager(
             state = pageState,
@@ -61,7 +90,7 @@ fun HomePlanPage() {
             when (it) {
                 0 -> HomePlanIndexPage()
                 1 -> HomePlanTotalPage()
-                else -> TODO()
+                else -> {}
             }
         }
     }

+ 0 - 19
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanTotalPage.kt

@@ -1,19 +0,0 @@
-package com.zaojiao.app.feat.home.plan
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.hilt.navigation.compose.hiltViewModel
-
-@Composable
-fun HomePlanTotalPage(
-) {
-    Box(
-        modifier = Modifier
-            .background(color = Color.Green)
-            .fillMaxSize(),
-    )
-}

+ 0 - 22
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanViewModel.kt

@@ -1,22 +0,0 @@
-package com.zaojiao.app.feat.home.plan
-
-import com.zaojiao.app.data.repo.StudyPlanRepository
-import com.zaojiao.app.feat.design.viewmodel.BaseViewModel
-import com.zaojiao.app.feat.home.plan.state.HomePlanIndexUiState
-import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
-
-@HiltViewModel
-class HomePlanViewModel @Inject constructor(
-    private val studyPlanRepository: StudyPlanRepository,
-) : BaseViewModel<HomePlanIndexUiState>() {
-    override suspend fun productState(): HomePlanIndexUiState {
-        val todayPlanList = studyPlanRepository.getTodayPlanList()
-        val tomorrowPlanList = studyPlanRepository.getTomorrowPlanList()
-
-        return HomePlanIndexUiState(
-            todayPlanList = todayPlanList,
-            tomorrowPlanList = tomorrowPlanList,
-        )
-    }
-}

+ 284 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/demo/HomePlanDemoPage.kt

@@ -0,0 +1,284 @@
+package com.zaojiao.app.feat.home.plan.demo
+
+import androidx.compose.runtime.Composable
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.Spring.StiffnessMediumLow
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.unit.sp
+import com.zaojiao.app.feat.design.Colors
+import com.zaojiao.app.feat.design.TabBar
+import com.zaojiao.app.feat.home.R
+import kotlinx.coroutines.launch
+
+@Composable
+fun HomePlanDemoPage() {
+    MaterialTheme() {
+        Column(
+            modifier = Modifier.fillMaxSize()
+        ) {
+//            TabBarView1()
+//            AnimatedChildAlignment(alignment = Alignment.Center)
+            TabBar(
+                tabs = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9"),
+                selectColor = Colors.from("#FFF2F0F3"),
+                unselectColor = Colors.from("#FFF5F6F8"),
+                selectStyle = TextStyle(
+                    color = Colors.from("#FF0744AE"),
+                    fontSize = 14.sp,
+                    lineHeight = 20.sp,
+                    fontWeight = FontWeight.Medium,
+                ),
+                unselectStyle = TextStyle(
+                    color = Colors.FF333333,
+                    fontSize = 14.sp,
+                    lineHeight = 20.sp,
+                ),
+            )
+        }
+    }
+}
+
+fun Modifier.animatePlacement(): Modifier = composed {
+    val scope = rememberCoroutineScope()
+    var targetOffset by remember { mutableStateOf(IntOffset.Zero) }
+    var animatable by remember {
+        mutableStateOf<Animatable<IntOffset, AnimationVector2D>?>(null)
+    }
+    this
+        .onPlaced {
+            // Calculate the position in the parent layout
+            targetOffset = it
+                .positionInParent()
+                .round()
+        }
+        .offset {
+            // Animate to the new target offset when alignment changes.
+            val anim = animatable ?: Animatable(targetOffset, IntOffset.VectorConverter)
+                .also { animatable = it }
+            if (anim.targetValue != targetOffset) {
+                scope.launch {
+                    anim.animateTo(targetOffset, spring(stiffness = StiffnessMediumLow))
+                }
+            }
+            // Offset the child in the opposite direction to the targetOffset, and slowly catch
+            // up to zero offset via an animation to achieve an overall animated movement.
+            animatable?.let { it.value - targetOffset } ?: IntOffset.Zero
+        }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun AnimatedChildAlignment(alignment: Alignment) {
+    Box(
+        Modifier
+            .fillMaxSize()
+            .padding(4.dp)
+            .border(1.dp, Color.Red)
+    ) {
+        Box(
+            modifier = Modifier
+                .animatePlacement()
+                .align(alignment)
+                .size(100.dp)
+                .background(Color.Red)
+        )
+    }
+}
+
+
+@Composable
+fun TabBarView1() {
+
+    val tabType = remember {
+        getTabTypes()
+    }
+
+    var tabState by remember {
+        mutableStateOf(tabType[0].type)
+    }
+
+    var index by remember {
+        mutableStateOf(0)
+    }
+
+    var parentOffset by remember {
+        mutableStateOf(IntSize.Zero)
+    }
+
+    val offsetAnimation: Dp by animateDpAsState(
+        ((parentOffset.width * (index + 1)) - (parentOffset.width / 2)).dp
+    )
+
+    Box(
+        modifier = Modifier
+            .padding(start = 24.dp, end = 24.dp)
+            .fillMaxWidth()
+            .wrapContentHeight()
+    ) {
+        Box(modifier = Modifier.wrapContentSize()) {
+
+            LazyRow(modifier = Modifier
+                .padding(top = 24.dp)
+                .fillMaxWidth()
+                .height(60.dp)
+                .shadow(elevation = 16.dp)
+                .clip(RoundedCornerShape(16.dp))
+                .background(MaterialTheme.colorScheme.background),
+                horizontalArrangement = Arrangement.SpaceEvenly,
+                verticalAlignment = Alignment.CenterVertically,
+                content = {
+                    items(tabType.size) { i ->
+                        TabBar1Image(
+                            modifier = Modifier.onGloballyPositioned {
+                                parentOffset = it.size
+                            },
+                            res = tabType[i].res,
+                            type = tabType[i].type,
+                            isSelected = tabState == tabType[i].type,
+                            onClick = {
+                                index = i
+                                tabState = it
+
+                            })
+                    }
+                })
+
+            Box(
+                modifier = Modifier
+                    .wrapContentSize()
+                    .absoluteOffset(x = offsetAnimation)
+            ) {
+                TabCircleInflated(tabType = tabType[index])
+            }
+
+        }
+    }
+}
+
+@Composable
+fun TabBar1Image(
+    @DrawableRes res: Int,
+    isInflated: Boolean = false,
+    isSelected: Boolean = false,
+    type: TabBar1111 = TabBar1111.Home,
+    onClick: (type: TabBar1111) -> Unit = {},
+    modifier: Modifier = Modifier,
+) {
+    Box(modifier = Modifier.size(24.dp)) {
+        AnimatedVisibility(
+            visible = isSelected.not(),
+            enter = slideInVertically(),
+            exit = slideOutVertically()
+        ) {
+            Icon(
+                modifier = modifier
+                    .clickable {
+                        onClick(type)
+                    },
+                painter = painterResource(id = res),
+                tint = if (isInflated) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onPrimary,
+                contentDescription = "tab"
+            )
+        }
+    }
+
+}
+
+@Composable
+fun TabCircleInflated(tabType: TabType = getTabTypes()[0]) {
+    Box(
+        modifier = Modifier
+            .size(64.dp)
+            .clip(CircleShape)
+            .background(MaterialTheme.colorScheme.background)
+            .padding(4.dp)
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .clip(CircleShape)
+                .background(MaterialTheme.colorScheme.onSurface),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            Box(modifier = Modifier) {
+                TabBar1Image(tabType.res, isInflated = true)
+            }
+        }
+    }
+}
+
+data class TabType(
+    val type: TabBar1111,
+    val res: Int,
+)
+
+enum class TabBar1111 {
+    Home, Game, Screen, Video
+}
+
+private fun getTabTypes() = listOf(
+    TabType(TabBar1111.Home, R.mipmap.personal_coupon),
+    TabType(TabBar1111.Game, R.mipmap.personal_account),
+    TabType(TabBar1111.Screen, R.mipmap.personal_feedback),
+    TabType(TabBar1111.Video, R.mipmap.personal_clockin)
+)
+
+
+@Preview(showBackground = true)
+@Composable
+fun TabCircleInflatedPreview() {
+    MaterialTheme() {
+        TabCircleInflated()
+    }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun TabBarView1Preview() {
+    MaterialTheme() {
+        TabBarView1()
+    }
+}

+ 199 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexHistory.kt

@@ -0,0 +1,199 @@
+package com.zaojiao.app.feat.home.plan.index
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+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.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
+import com.zaojiao.app.feat.design.Colors
+import com.zaojiao.app.feat.design.Expanded
+import com.zaojiao.app.feat.design.Icons
+import com.zaojiao.app.feat.design.Images
+import com.zaojiao.app.feat.design.Spacer
+import com.zaojiao.app.feat.design.list
+import com.zaojiao.app.feat.home.R
+
+@Composable
+internal fun HomePlanIndexHistoryTitle() {
+    Row(
+        modifier = Modifier
+            .padding(horizontal = 16.dp)
+            .fillMaxWidth()
+            .height(28.dp),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        Text(
+            text = "历史计划",
+            style = TextStyle(
+                color = Colors.FF999999,
+                fontSize = 20.sp,
+                fontWeight = FontWeight.SemiBold,
+                lineHeight = 28.sp,
+            ),
+        )
+        Expanded()
+        Text(
+            text = "全部计划",
+            style = TextStyle(
+                color = Colors.FF999999,
+                fontSize = 14.sp,
+                lineHeight = 16.sp,
+            ),
+        )
+        Spacer(width = 6.dp)
+        Icons.Forward(
+            size = Size(width = 6f, height = 10.5f),
+            color = Colors.FF999999,
+            width = 1.dp,
+        )
+    }
+}
+
+@Composable
+internal fun HomePlanIndexHistoryTab(
+    historyCategory: List<CategoryModel>,
+    onChoose: (category: CategoryModel) -> Unit,
+    modifier: Modifier,
+) {
+    LocalWindowInfo
+    LazyRow(
+        modifier = Modifier
+            .then(modifier)
+            .background(color = Colors.from("#FFF9F9F9"))
+            .padding(vertical = 10.dp)
+            .fillMaxWidth()
+            .height(30.dp),
+    ) {
+        item {
+            Box(modifier = Modifier.width(16.dp))
+        }
+        list(count = historyCategory.size,
+            itemContent = { index ->
+                Text(
+                    text = historyCategory[index].name,
+                    modifier = Modifier
+                        .clickable {
+                            onChoose.invoke(historyCategory[index])
+                        }
+                        .background(
+                            color = Colors.from("#FFF2F0F3"),
+                            shape = RoundedCornerShape(15.dp),
+                        )
+                        .padding(horizontal = 12.dp)
+                        .wrapContentWidth()
+                        .fillMaxHeight(),
+                )
+            },
+            itemSeparation = {
+                Box(modifier = Modifier.width(16.dp))
+            }
+        )
+        item {
+            Box(modifier = Modifier.width(16.dp))
+        }
+    }
+}
+
+@Composable
+fun HomePlanIndexHistoryItem(historyModel: StudyPlanHistoryModel) {
+    Box(
+        modifier = Modifier
+            .padding(vertical = 6.dp)
+            .padding(horizontal = 16.dp)
+            .background(
+                color = Color.White,
+                shape = RoundedCornerShape(20.dp),
+            )
+            .fillMaxWidth()
+            .height(150.dp)
+    ) {
+        Row(
+            modifier = Modifier
+                .padding(12.dp)
+                .fillMaxSize(),
+        ) {
+            Images.Network(
+                url = historyModel.courseItemImgCover,
+                modifier = Modifier
+                    .clip(shape = RoundedCornerShape(20.dp))
+                    .fillMaxHeight()
+                    .width(96.dp),
+            )
+
+            Spacer(width = 12.dp)
+
+            Column(modifier = Modifier.weight(1f)) {
+                Spacer(height = 16.dp)
+                Text(
+                    text = historyModel.courseItemName,
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 1,
+                    style = TextStyle(
+                        color = Colors.FF666666,
+                        fontSize = 14.sp,
+                        lineHeight = 16.sp,
+                    ),
+                )
+                Spacer(height = 10.dp)
+                Text(
+                    text = historyModel.courseItemName,
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 1,
+                    style = TextStyle(
+                        color = Colors.FF333333,
+                        fontSize = 18.sp,
+                        lineHeight = 21.sp,
+                        fontWeight = FontWeight.Medium,
+                    ),
+                )
+                Spacer(height = 12.dp)
+                Row() {
+                    repeat(historyModel.totalTask) {
+                        if (it != 0) {
+                            Spacer(width = 8.dp)
+                        }
+                        if (it < historyModel.completeTaskNum) {
+                            Images.Resource(
+                                id = R.mipmap.study_plan_light_on,
+                                modifier = Modifier
+                                    .width(17.dp)
+                                    .height(22.dp),
+                            )
+                        } else {
+                            Images.Resource(
+                                id = R.mipmap.study_plan_light_off,
+                                modifier = Modifier
+                                    .width(17.dp)
+                                    .height(22.dp),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 637 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexPage.kt

@@ -0,0 +1,637 @@
+package com.zaojiao.app.feat.home.plan.index
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.aspectRatio
+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.heightIn
+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.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
+import com.zaojiao.app.feat.design.Colors
+import com.zaojiao.app.feat.design.Expanded
+import com.zaojiao.app.feat.design.Images
+import com.zaojiao.app.feat.design.Spacer
+import com.zaojiao.app.feat.design.StatePage
+import com.zaojiao.app.feat.home.R
+import com.zaojiao.app.feat.home.plan.state.HomePlanIndexUiState
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun HomePlanIndexPage(
+    viewModel: HomePlanIndexViewModel = hiltViewModel()
+) {
+    val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
+
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize(),
+    ) {
+        StatePage(uiState = uiState) { state ->
+            var bodyHeight by remember { mutableStateOf(2000) }
+            var tabHeight by remember { mutableStateOf(0) }
+
+            val historyCategory = state.historyCategory
+            val historyPlanList = state.historyPlanList
+
+            LazyColumn(modifier = Modifier
+                .onPlaced { bodyHeight = it.size.height }
+                .fillMaxSize()) {
+                if (state == HomePlanIndexUiState.logout) {
+                    item {
+                        Column(
+                            modifier = Modifier
+                                .padding(vertical = 60.dp)
+                                .fillMaxWidth()
+                                .wrapContentHeight(),
+                        ) {
+                            Images.Resource(
+                                id = R.mipmap.study_plan_no_item,
+                                modifier = Modifier
+                                    .align(Alignment.CenterHorizontally)
+                                    .size(172.dp, 140.dp),
+                            )
+
+                            Text(
+                                text = "当前还未购买,快点来购买吧!",
+                                modifier = Modifier.align(Alignment.CenterHorizontally)
+                            )
+                        }
+                    }
+                } else {
+                    if (state.tomorrowPlanList.isNotEmpty()) {
+                        item {
+                            HomePlanIndexTomorrow(
+                                tomorrowPlanList = state.tomorrowPlanList,
+                            )
+                        }
+                    }
+
+                    item {
+                        Box(modifier = Modifier.height(20.dp))
+                    }
+
+                    item {
+                        HomePlanIndexToday(
+                            todayPlanList = state.todayPlanList,
+                        )
+                    }
+
+                    item {
+                        Box(modifier = Modifier.height(25.dp))
+                    }
+
+                    item {
+                        HomePlanIndexHistoryTitle()
+                    }
+
+                    item {
+                        Box(modifier = Modifier.height(10.dp))
+                    }
+
+                    stickyHeader {
+                        HomePlanIndexHistoryTab(
+                            historyCategory = state.historyCategory,
+                            onChoose = viewModel::selectCategory,
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    tabHeight = it.size.height
+                                },
+                        )
+                    }
+
+
+                    item {
+                        val height = bodyHeight - tabHeight
+
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .heightIn(
+                                    min = (height / LocalDensity.current.density).dp
+                                ),
+                        ) {
+                            Spacer(height = 6.dp)
+                            repeat(historyPlanList.size) {
+                                val historyModel = historyPlanList[it]
+
+                                HomePlanIndexHistoryItem(historyModel)
+                            }
+                            Spacer(height = 6.dp)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun HomePlanIndexTomorrow(
+    tomorrowPlanList: List<StudyPlanTomorrowModel>
+) {
+    val count = tomorrowPlanList.size
+    val pagerState = rememberPagerState()
+
+    HorizontalPager(
+        pageCount = count,
+        state = pagerState,
+        modifier = Modifier
+            .padding(horizontal = 16.dp)
+            .clip(RoundedCornerShape(35.dp))
+            .fillMaxWidth()
+            .height(70.dp)
+    ) {
+        HomePlanTomorrowItem(tomorrowPlanList[it])
+    }
+
+    if (count > 1) {
+        Row(
+            modifier = Modifier
+                .padding(top = 8.dp)
+                .fillMaxWidth()
+                .height(4.dp),
+            horizontalArrangement = Arrangement.Center,
+        ) {
+            repeat(count) {
+                if (it == pagerState.currentPage) {
+                    Box(
+                        modifier = Modifier
+                            .padding(horizontal = 2.dp)
+                            .background(
+                                color = Colors.from("#d8d8d8"),
+                                shape = RoundedCornerShape(2.dp),
+                            )
+                            .fillMaxHeight()
+                            .width(16.dp),
+                    )
+                } else {
+                    Box(
+                        modifier = Modifier
+                            .padding(horizontal = 2.dp)
+                            .background(
+                                color = Colors.from("#d8d8d8"),
+                                shape = RoundedCornerShape(2.dp),
+                            )
+                            .height(4.dp)
+                            .width(4.dp),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun HomePlanIndexToday(
+    todayPlanList: List<StudyPlanTodayModel>
+) {
+    Box(
+        modifier = Modifier
+            .padding(horizontal = 16.dp)
+            .fillMaxWidth()
+            .height(227.dp),
+    ) {
+        Box(
+            modifier = Modifier
+                .background(
+                    brush = Brush.horizontalGradient(
+                        colors = listOf(
+                            Colors.from("#EBFEE6D5"),
+                            Colors.from("#FFFFE7D6"),
+                        )
+                    ),
+                    shape = RoundedCornerShape(20.dp),
+                )
+                .fillMaxSize()
+        )
+
+        Column(
+            modifier = Modifier.fillMaxSize(),
+        ) {
+            Row(
+                modifier = Modifier
+                    .padding(horizontal = 12.dp, vertical = 20.dp)
+                    .fillMaxWidth()
+                    .wrapContentHeight(),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                Text(
+                    text = "今日计划",
+                    style = TextStyle(
+                        color = Colors.from("#FFFF8024"),
+                        fontWeight = FontWeight.SemiBold,
+                        fontSize = 20.sp,
+                        lineHeight = 28.sp,
+                    ),
+                )
+
+                Spacer(width = 6.dp)
+
+                Text(
+                    text = "共${todayPlanList.size}节",
+                    style = TextStyle(
+                        color = Colors.from("#FFFF8024"),
+                        fontWeight = FontWeight.Medium,
+                        fontSize = 11.sp,
+                        lineHeight = 12.sp,
+                    ),
+                    modifier = Modifier
+                        .background(
+                            color = Colors
+                                .from("#FFFF8024")
+                                .copy(alpha = 0.2f), shape = RoundedCornerShape(
+                                topStart = 15.dp,
+                                bottomStart = 2.dp,
+                                topEnd = 15.dp,
+                                bottomEnd = 15.dp,
+                            )
+                        )
+                        .padding(horizontal = 7.dp, vertical = 3.dp)
+                        .wrapContentSize(),
+                )
+
+                Expanded()
+                Text(
+                    text = "计划日历",
+                    style = TextStyle(
+                        color = Colors.from("#FFFF8024"),
+                        fontWeight = FontWeight.Normal,
+                        fontSize = 14.sp,
+                        lineHeight = 16.sp,
+                    ),
+                )
+            }
+
+            LazyRow(
+                modifier = Modifier
+                    .weight(1f)
+                    .fillMaxWidth(),
+            ) {
+                item {
+                    Box(modifier = Modifier.width(6.dp))
+                }
+                todayPlanList.forEach {
+                    item {
+                        HomePlanTodayItem(it)
+                    }
+                }
+                item {
+                    Box(modifier = Modifier.width(6.dp))
+                }
+            }
+
+            Spacer(height = 20.dp)
+        }
+    }
+}
+
+@Composable
+private fun HomePlanTomorrowItem(tomorrow: StudyPlanTomorrowModel) {
+    Box(
+        modifier = Modifier
+            .clip(RoundedCornerShape(35.dp))
+            .fillMaxSize(),
+    ) {
+        Images.Color(
+            color = Colors.from("#FFFF8024"),
+            description = "",
+            modifier = Modifier.fillMaxSize(),
+        )
+
+        Row(modifier = Modifier.fillMaxSize()) {
+            Box(
+                modifier = Modifier
+                    .padding(6.dp)
+                    .border(
+                        width = 1.dp,
+                        color = Color.White,
+                        shape = RoundedCornerShape(35.dp),
+                    )
+                    .clip(RoundedCornerShape(35.dp))
+                    .aspectRatio(ratio = 1f),
+            ) {
+                Images.Network(
+                    url = tomorrow.courseItemImgCover, modifier = Modifier.fillMaxSize()
+                )
+
+                Box(
+                    modifier = Modifier
+                        .background(color = Colors.from("#36000000"))
+                        .fillMaxSize(),
+                )
+
+                Images.Resource(
+                    id = R.mipmap.study_plan_lock_white,
+                    modifier = Modifier
+                        .align(Alignment.Center)
+                        .size(26.dp),
+                )
+            }
+
+            Column(
+                modifier = Modifier
+                    .align(Alignment.CenterVertically)
+                    .weight(1f)
+                    .padding(horizontal = 9.dp)
+                    .wrapContentHeight()
+            ) {
+                Text(
+                    text = tomorrow.courseName,
+                    style = TextStyle(
+                        color = Color.White,
+                        fontWeight = FontWeight.Normal,
+                        fontSize = 14.sp,
+                        lineHeight = 16.sp,
+                    ),
+                )
+                Spacer(height = 6.dp)
+                Text(
+                    text = tomorrow.courseItemName,
+                    style = TextStyle(
+                        color = Color.White,
+                        fontWeight = FontWeight.Medium,
+                        fontSize = 18.sp,
+                        lineHeight = 21.sp,
+                    ),
+                )
+            }
+
+            Box(
+                modifier = Modifier
+                    .align(Alignment.CenterVertically)
+                    .background(
+                        color = Colors.from("#CDFFFFFF"),
+                        shape = RoundedCornerShape(20.dp),
+                    )
+                    .padding(
+                        start = 12.dp,
+                        top = 7.dp,
+                        end = 9.dp,
+                        bottom = 6.dp,
+                    )
+                    .wrapContentSize()
+            ) {
+                if (tomorrow.tomorrowUnlock) {
+                    Text(
+                        text = "明日解锁",
+                        style = TextStyle(
+                            color = Colors.from("#FFFF8024"),
+                            fontWeight = FontWeight.Normal,
+                            fontSize = 12.sp,
+                            lineHeight = 12.sp,
+                        ),
+                    )
+                } else {
+                    Text(
+                        text = "${tomorrow.unlockDate.substring(0, 5)}解锁",
+                        style = TextStyle(
+                            color = Colors.from("#FFFF8024"),
+                            fontWeight = FontWeight.Normal,
+                            fontSize = 12.sp,
+                            lineHeight = 12.sp,
+                        ),
+                    )
+                }
+            }
+            Spacer(width = 16.dp)
+        }
+    }
+}
+
+@Composable
+private fun HomePlanTodayItem(today: StudyPlanTodayModel) {
+    Box(
+        modifier = Modifier
+            .padding(horizontal = 6.dp)
+            .fillMaxHeight()
+            .width(262.dp),
+    ) {
+        Box(
+            modifier = Modifier
+                .padding(top = 20.dp)
+                .background(
+                    color = Color.White, shape = RoundedCornerShape(20.dp)
+                )
+                .fillMaxHeight()
+                .fillMaxWidth()
+        )
+
+        Box(
+            modifier = Modifier
+                .padding(start = 9.dp, bottom = 9.dp)
+                .align(Alignment.TopStart)
+                .border(
+                    width = 3.dp, color = Color.White, shape = RoundedCornerShape(14.dp)
+                )
+                .background(
+                    color = Color.Green.copy(alpha = 0.3f),
+                    shape = RoundedCornerShape(14.dp),
+                )
+                .fillMaxHeight()
+                .width(96.dp)
+        ) {
+            Images.Network(
+                url = today.courseItemImgCover,
+                modifier = Modifier
+                    .clip(RoundedCornerShape(14.dp))
+                    .fillMaxSize(),
+            )
+
+            Text(
+                text = today.courseCategoryName,
+                style = TextStyle(
+                    color = Color.White,
+                    fontWeight = FontWeight.SemiBold,
+                    fontSize = 10.sp,
+                    lineHeight = 10.sp,
+                ),
+                modifier = Modifier
+                    .align(Alignment.TopStart)
+                    .padding(3.dp)
+                    .drawWithContent {
+                        this.drawContent()
+                        val radius = 3.dp.toPx()
+                        val radius2 = 6.dp.toPx()
+
+                        val width = this.size.width
+                        val height = this.size.height
+
+                        val path = Path().apply {
+                            moveTo(width + radius, 0f)
+                            arcTo(
+                                rect = Rect(
+                                    center = Offset(width + radius, radius),
+                                    radius = radius,
+                                ),
+                                startAngleDegrees = -100f,
+                                sweepAngleDegrees = -90f,
+                                forceMoveTo = true,
+                            )
+                            lineTo(width, height - radius)
+                            arcTo(
+                                rect = Rect(
+                                    center = Offset(width - radius2, height - radius2),
+                                    radius = radius2,
+                                ),
+                                startAngleDegrees = 0f,
+                                sweepAngleDegrees = 80f,
+                                forceMoveTo = true,
+                            )
+                            lineTo(radius / 2, height)
+                            arcTo(
+                                rect = Rect(
+                                    center = Offset(radius / 2, height + radius),
+                                    radius = radius,
+                                ),
+                                startAngleDegrees = -90f,
+                                sweepAngleDegrees = -90f,
+                                forceMoveTo = true,
+                            )
+                        }
+
+                        this.drawPath(
+                            path = path, color = Color.White, style = Stroke(
+                                width = 3.dp.toPx(),
+                            )
+                        )
+                    }
+                    .background(
+                        color = Colors.from("#FFFF8024"), shape = RoundedCornerShape(
+                            topStart = 10.dp,
+                            bottomStart = 4.dp,
+                            topEnd = 0.dp,
+                            bottomEnd = 0.dp,
+                        )
+                    )
+                    .padding(horizontal = 7.dp, vertical = 3.dp)
+                    .wrapContentSize(),
+            )
+        }
+        Column(
+            modifier = Modifier
+                .padding(
+                    start = 117.dp,
+                    top = 20.dp,
+                    end = 12.dp,
+                )
+                .fillMaxHeight()
+                .fillMaxWidth()
+        ) {
+            Spacer(height = 15.dp)
+            Text(
+                text = today.courseName,
+                overflow = TextOverflow.Ellipsis,
+                maxLines = 1,
+                style = TextStyle(
+                    color = Colors.FF666666,
+                    fontWeight = FontWeight.Normal,
+                    fontSize = 14.sp,
+                    lineHeight = 16.sp,
+                ),
+            )
+            Spacer(height = 5.dp)
+            Text(
+                text = today.courseItemName,
+                overflow = TextOverflow.Ellipsis,
+                maxLines = 1,
+                style = TextStyle(
+                    color = Colors.FF333333,
+                    fontWeight = FontWeight.Medium,
+                    fontSize = 18.sp,
+                    lineHeight = 21.sp,
+                ),
+            )
+            Spacer(height = 8.dp)
+
+            if (today.unlockState) {
+                Row() {
+                    repeat(today.totalTask) {
+                        if (it != 0) {
+                            Spacer(width = 8.dp)
+                        }
+                        if (it < today.completeTaskNum) {
+                            Images.Resource(
+                                id = R.mipmap.study_plan_light_on,
+                                modifier = Modifier
+                                    .width(17.dp)
+                                    .height(22.dp),
+                            )
+                        } else {
+                            Images.Resource(
+                                id = R.mipmap.study_plan_light_off,
+                                modifier = Modifier
+                                    .width(17.dp)
+                                    .height(22.dp),
+                            )
+                        }
+                    }
+                }
+            } else {
+                Row() {
+                    Images.Resource(
+                        id = R.mipmap.study_plan_lock,
+                        modifier = Modifier
+                            .width(16.dp)
+                            .height(16.dp),
+                    )
+                    Spacer(width = 4.dp)
+                    Text(
+                        text = "${today.unlockTime}解锁",
+                        style = TextStyle(
+                            color = Colors.FF333333,
+                            fontWeight = FontWeight.Normal,
+                            fontSize = 14.sp,
+                            lineHeight = 16.sp,
+                        ),
+                    )
+                }
+            }
+        }
+    }
+}

+ 84 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/index/HomePlanIndexViewModel.kt

@@ -0,0 +1,84 @@
+package com.zaojiao.app.feat.home.plan.index
+
+import androidx.lifecycle.viewModelScope
+import com.zaojiao.app.core.auth.data.AuthRepository
+import com.zaojiao.app.core.auth.utils.AuthState
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
+import com.zaojiao.app.data.repo.StudyPlanRepository
+import com.zaojiao.app.feat.design.viewmodel.BaseViewModel
+import com.zaojiao.app.feat.home.plan.state.HomePlanIndexUiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class HomePlanIndexViewModel @Inject constructor(
+    private val authRepository: AuthRepository,
+    private val studyPlanRepository: StudyPlanRepository,
+) : BaseViewModel<HomePlanIndexUiState>() {
+    private var lastUiState: HomePlanIndexUiState? = null
+
+    private var selectCategory: CategoryModel? = null
+
+    private val historyListMap = HashMap<CategoryModel, List<StudyPlanHistoryModel>>()
+
+    init {
+        viewModelScope.launch {
+            authRepository.state.collectLatest {
+                when (it) {
+                    AuthState.IN -> {
+                        initialState()
+                    }
+
+                    AuthState.OUT -> {
+                        setSuccess(HomePlanIndexUiState.logout)
+                    }
+                }
+            }
+        }
+    }
+
+    override suspend fun productState(): HomePlanIndexUiState {
+        val todayPlanList = studyPlanRepository.getTodayPlanList()
+        val tomorrowPlanList = studyPlanRepository.getTomorrowPlanList()
+
+        val historyCategory = studyPlanRepository.getHistoryCategory()
+        var historyPlanList: List<StudyPlanHistoryModel> = emptyList()
+
+        historyCategory.apply {
+            if (this.isNotEmpty()) {
+                selectCategory = this.first().apply {
+                    historyPlanList = studyPlanRepository.getHistoryPlanList(this)
+                    historyListMap[this] = historyPlanList
+                }
+            }
+        }
+
+        return HomePlanIndexUiState(
+            todayPlanList = todayPlanList,
+            tomorrowPlanList = tomorrowPlanList,
+            historyCategory = historyCategory,
+            historyPlanList = historyPlanList,
+        ).apply {
+            lastUiState = this
+        }
+    }
+
+    fun selectCategory(categoryModel: CategoryModel) = viewModelScope.launch {
+        if (selectCategory != categoryModel) {
+            selectCategory = categoryModel
+
+            val historyPlanList = studyPlanRepository.getHistoryPlanList(categoryModel)
+            historyListMap[categoryModel] = historyPlanList
+
+            lastUiState?.copy(
+                historyPlanList = historyPlanList,
+            )?.apply {
+                setSuccess(this)
+                lastUiState = this
+            }
+        }
+    }
+}

+ 7 - 3
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/state/HomePlanIndexUiState.kt

@@ -1,14 +1,18 @@
 package com.zaojiao.app.feat.home.plan.state
 
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanHistoryModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTodayModel
 import com.zaojiao.app.data.model.studyplan.StudyPlanTomorrowModel
 
 data class HomePlanIndexUiState(
     val todayPlanList: List<StudyPlanTodayModel> = emptyList(),
     val tomorrowPlanList: List<StudyPlanTomorrowModel> = emptyList(),
-    val historyPlanList: List<String> = emptyList(),
-){
-    companion object{
+    val historyCategory: List<CategoryModel> = emptyList(),
+    val historyPlanList: List<StudyPlanHistoryModel> = emptyList(),
+) {
+    companion object {
         val logout = HomePlanIndexUiState()
     }
 }

+ 2 - 3
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/state/HomePlanTotalUiState.kt

@@ -1,5 +1,4 @@
 package com.zaojiao.app.feat.home.plan.state
 
-data class HomePlanTotalUiState(
-    val categoryList: List<String> = emptyList(),
-)
+import com.zaojiao.app.data.model.common.CategoryModel
+

+ 261 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/total/HomePlanTotalPage.kt

@@ -0,0 +1,261 @@
+package com.zaojiao.app.feat.home.plan.total
+
+import androidx.compose.foundation.background
+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.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.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.common.MediaCategoryModel
+import com.zaojiao.app.data.model.common.SortCategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.feat.design.Colors
+import com.zaojiao.app.feat.design.Images
+import com.zaojiao.app.feat.design.Spacer
+import com.zaojiao.app.feat.design.StatePage
+import com.zaojiao.app.feat.design.TabBar
+import com.zaojiao.app.feat.design.onState
+import com.zaojiao.app.feat.home.R
+
+@Composable
+fun HomePlanTotalPage(
+    viewModel: HomePlanTotalViewModel = hiltViewModel()
+) {
+    val uiState by viewModel.stateFlow.collectAsState()
+    val courseList = viewModel.pagingData.collectAsLazyPagingItems()
+
+    StatePage(uiState = uiState) { state ->
+        Column(
+            modifier = Modifier.fillMaxSize(),
+        ) {
+            HomePlanTotalCategory(
+                mediaTypeList = state.mediaTypeList,
+                sortTypeList = state.sortTypeList,
+                categoryList = state.categoryList,
+                initialMediaType = state.selectMediaType,
+                initialSortType = state.selectSortType,
+                initialCategory = state.selectCategory,
+                onChangeMediaType = {
+                    viewModel.sendAction(HomePlanTotalAction.MediaType(state.mediaTypeList[it]))
+                },
+                onChangeSortType = {
+                    viewModel.sendAction(HomePlanTotalAction.SortType(state.sortTypeList[it]))
+                },
+                onChangeCategory = {
+                    viewModel.sendAction(HomePlanTotalAction.Category(state.categoryList[it]))
+                },
+            )
+
+            Box(
+                modifier = Modifier
+                    .weight(1f)
+                    .fillMaxSize(),
+            ) {
+                courseList.onState { list ->
+                    LazyColumn(
+                        modifier = Modifier.fillMaxSize(),
+                    ) {
+                        items(list.itemCount) {
+                            val model = list[it] ?: return@items
+                            HomePlanTotalCourseItem(model)
+                        }
+                    }
+                }
+            }
+
+        }
+    }
+}
+
+@Composable
+fun HomePlanTotalCategory(
+    mediaTypeList: List<MediaCategoryModel>,
+    sortTypeList: List<SortCategoryModel>,
+    categoryList: List<CategoryModel>,
+    initialMediaType: Int,
+    initialSortType: Int,
+    initialCategory: Int,
+    onChangeMediaType: (Int) -> Unit,
+    onChangeSortType: (Int) -> Unit,
+    onChangeCategory: (Int) -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .background(
+                brush = Brush.verticalGradient(
+                    colors = listOf(
+                        Colors.from("#00FFFFFF"),
+                        Colors.from("#FFFFFFFF"),
+                    ),
+                ),
+                shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp)
+            )
+            .wrapContentHeight()
+            .fillMaxWidth(),
+    ) {
+
+        TabBar(
+            initial = initialMediaType,
+            tabs = mediaTypeList.map { model -> model.typeName },
+            selectColor = Colors.from("#FFF2F0F3"),
+            unselectColor = Colors.from("#FFF5F6F8"),
+            selectStyle = TextStyle(
+                color = Colors.from("#FF0744AE"),
+                fontSize = 14.sp,
+                lineHeight = 20.sp,
+                fontWeight = FontWeight.Medium,
+            ),
+            unselectStyle = TextStyle(
+                color = Colors.FF333333,
+                fontSize = 14.sp,
+                lineHeight = 20.sp,
+            ),
+            onChange = onChangeMediaType,
+        )
+
+        Spacer(height = 16.dp)
+
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Box(
+                modifier = Modifier
+                    .weight(1f)
+                    .wrapContentHeight(),
+            ) {
+                TabBar(
+                    initial = initialSortType,
+                    tabs = sortTypeList.map { model -> model.typeName },
+                    selectColor = Colors.from("#FFF2F0F3"),
+                    unselectColor = Colors.from("#FFF5F6F8"),
+                    selectStyle = TextStyle(
+                        color = Colors.from("#FF0744AE"),
+                        fontSize = 14.sp,
+                        lineHeight = 20.sp,
+                        fontWeight = FontWeight.Medium,
+                    ),
+                    unselectStyle = TextStyle(
+                        color = Colors.FF333333,
+                        fontSize = 14.sp,
+                        lineHeight = 20.sp,
+                    ),
+                    onChange = onChangeSortType,
+                )
+            }
+
+            Images.Resource(
+                id = R.mipmap.personal_clockin,
+                modifier = Modifier.size(16.dp),
+            )
+        }
+
+        Spacer(height = 16.dp)
+
+        TabBar(
+            initial = initialCategory,
+            tabs = categoryList.map { model -> model.name },
+            selectColor = Colors.from("#FFF2F0F3"),
+            unselectColor = Colors.from("#FFF5F6F8"),
+            selectStyle = TextStyle(
+                color = Colors.from("#FF0744AE"),
+                fontSize = 14.sp,
+                lineHeight = 20.sp,
+                fontWeight = FontWeight.Medium,
+            ),
+            unselectStyle = TextStyle(
+                color = Colors.FF333333,
+                fontSize = 14.sp,
+                lineHeight = 20.sp,
+            ),
+            onChange = onChangeCategory,
+        )
+
+        Spacer(height = 16.dp)
+    }
+}
+
+@Composable
+fun HomePlanTotalCourseItem(model: StudyPlanCourseItemModel) {
+    Row(
+        modifier = Modifier
+            .padding(horizontal = 16.dp)
+            .padding(vertical = 6.dp)
+            .clip(shape = RoundedCornerShape(20.dp))
+            .background(color = Color.White)
+            .padding(12.dp)
+            .height(IntrinsicSize.Min)
+            .fillMaxWidth(),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        Images.Network(
+            url = model.courseImgCoverMini,
+            modifier = Modifier
+                .clip(shape = RoundedCornerShape(20.dp))
+                .size(110.dp),
+        )
+        Spacer(width = 12.dp)
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .wrapContentHeight(),
+        ) {
+            Text(
+                text = model.courseName,
+                maxLines = 2,
+                overflow = TextOverflow.Ellipsis,
+                style = TextStyle(
+                    color = Colors.FF333333,
+                    fontSize = 18.sp,
+                    lineHeight = 21.sp,
+                    fontWeight = FontWeight.Medium,
+                )
+            )
+            Spacer(height = 12.dp)
+            Text(
+                text = "已完成${model.completeItemQuantity}/${model.totalItemQuantity}小节",
+                style = TextStyle(
+                    color = Colors.from("#FFFF862F"),
+                    fontSize = 12.sp,
+                    lineHeight = 14.sp,
+                    fontWeight = FontWeight.Medium,
+                ),
+                modifier = Modifier
+                    .background(
+                        color = Colors
+                            .from("#FFFF8024")
+                            .copy(alpha = 0.18f),
+                        shape = RoundedCornerShape(15.dp),
+                    )
+                    .padding(vertical = 7.dp, horizontal = 10.dp)
+                    .wrapContentWidth()
+                    .wrapContentHeight(),
+            )
+        }
+    }
+}

+ 105 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/total/HomePlanTotalViewModel.kt

@@ -0,0 +1,105 @@
+package com.zaojiao.app.feat.home.plan.total
+
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.zaojiao.app.data.model.common.CategoryModel
+import com.zaojiao.app.data.model.common.MediaCategoryModel
+import com.zaojiao.app.data.model.common.SortCategoryModel
+import com.zaojiao.app.data.model.studyplan.StudyPlanCourseItemModel
+import com.zaojiao.app.data.repo.StudyPlanRepository
+import com.zaojiao.app.feat.design.viewmodel.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+data class HomePlanTotalUiState(
+    val mediaTypeList: List<MediaCategoryModel> = emptyList(),
+    val sortTypeList: List<SortCategoryModel> = emptyList(),
+    val categoryList: List<CategoryModel> = emptyList(),
+    val selectMediaType: Int = 0,
+    val selectSortType: Int = 0,
+    val selectCategory: Int = 0,
+)
+
+sealed class HomePlanTotalAction {
+    data class MediaType(
+        val mediaCategoryModel: MediaCategoryModel
+    ) : HomePlanTotalAction()
+
+    data class SortType(
+        val sortCategoryModel: SortCategoryModel,
+    ) : HomePlanTotalAction()
+
+    data class Category(
+        val categoryModel: CategoryModel,
+    ) : HomePlanTotalAction()
+}
+
+private data class Query(
+    val mediaType: String?,
+    val sortType: String,
+    val category: String,
+)
+
+@HiltViewModel
+class HomePlanTotalViewModel @Inject constructor(
+    private val studyPlanRepository: StudyPlanRepository,
+) : BaseViewModel<HomePlanTotalUiState>() {
+    val pagingData: Flow<PagingData<StudyPlanCourseItemModel>>
+
+    val sendAction: (HomePlanTotalAction) -> Unit
+
+    init {
+        val mediaTypeFlow = MutableSharedFlow<MediaCategoryModel>()
+        val sortTypeFlow = MutableSharedFlow<SortCategoryModel>()
+        val categoryFlow = MutableSharedFlow<CategoryModel>()
+
+        pagingData = combine(mediaTypeFlow, sortTypeFlow, categoryFlow) { c1, c2, c3 ->
+            Query(c1.mediaType, c2.sortType, c3.id)
+        }.distinctUntilChanged().flatMapLatest {
+            studyPlanRepository.getStudyPlanCourse(it.mediaType, it.sortType, it.category)
+        }.cachedIn(viewModelScope)
+
+        sendAction = { action ->
+            viewModelScope.launch {
+                when (action) {
+                    is HomePlanTotalAction.Category -> {
+                        categoryFlow.emit(action.categoryModel)
+                    }
+
+                    is HomePlanTotalAction.MediaType -> {
+                        mediaTypeFlow.emit(action.mediaCategoryModel)
+                    }
+
+                    is HomePlanTotalAction.SortType -> {
+                        sortTypeFlow.emit(action.sortCategoryModel)
+                    }
+                }
+            }
+        }
+
+        initialState()
+    }
+
+    override suspend fun productState(): HomePlanTotalUiState {
+        val mediaTypeList = studyPlanRepository.getCourseMediaCategory()
+        val sortTypeList = studyPlanRepository.getCourseSortCategory()
+        val categoryList = studyPlanRepository.getCourseTypeCategory()
+
+        return HomePlanTotalUiState(
+            mediaTypeList = mediaTypeList,
+            sortTypeList = sortTypeList,
+            categoryList = categoryList,
+        ).apply {
+            sendAction(HomePlanTotalAction.Category(categoryList[selectCategory]))
+            sendAction(HomePlanTotalAction.MediaType(mediaTypeList[selectMediaType]))
+            sendAction(HomePlanTotalAction.SortType(sortTypeList[selectSortType]))
+        }
+    }
+}

+ 216 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/widget/HomePlanTabBar.kt

@@ -0,0 +1,216 @@
+package com.zaojiao.app.feat.home.plan.widget
+
+import android.util.Log
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+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.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.DrawStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+
+@Immutable
+private class TabItemPosition(val left: Dp, val width: Dp) {
+    val right: Dp get() = left + width
+
+    val center: Dp get() = left + width / 2
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TabItemPosition) return false
+
+        if (left != other.left) return false
+        if (width != other.width) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = left.hashCode()
+        result = 31 * result + width.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "TabItemPosition(left=$left, right=$right, width=$width)"
+    }
+}
+
+@Composable
+fun HomePlanTabBar(
+    modifier: Modifier = Modifier,
+    initial: Int = 0,
+    tabs: List<String>,
+    selectStyle: TextStyle,
+    unselectStyle: TextStyle,
+    onChange: ((Int) -> Unit)? = null,
+) {
+    val density = LocalDensity.current.density
+
+    val coroutineScope = rememberCoroutineScope()
+
+    val lazyListState = rememberLazyListState()
+
+    var lazyListWidth by remember { mutableStateOf(0) }
+
+    val itemPositions = remember { MutableList(tabs.size) { TabItemPosition(0.dp, 0.dp) } }
+
+    var selectIndex by remember { mutableStateOf(initial) }
+
+    var updated by remember { mutableStateOf(false) }
+
+    var lastPosition by remember { mutableStateOf(0.dp) }
+
+    val indicatorPosition by animateDpAsState(
+        if (updated) itemPositions[selectIndex].center else (-100).dp
+    ) {
+        lastPosition = it
+    }
+
+    Column(
+        modifier = modifier
+            .padding(horizontal = 16.dp, vertical = 16.dp)
+            .wrapContentHeight(),
+    ) {
+        LazyRow(
+            state = lazyListState,
+            modifier = Modifier
+                .onGloballyPositioned {
+                    lazyListWidth = it.size.width
+                }
+                .fillMaxWidth()
+                .wrapContentHeight(),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            items(tabs.size) { index ->
+                TabBarItem(
+                    title = tabs[index],
+                    style = if (index == selectIndex) selectStyle else unselectStyle,
+                    modifier = Modifier
+                        .padding(horizontal = 8.dp)
+                        .onGloballyPositioned {
+                            val left = it
+                                .positionInParent()
+                                .round().x
+                            val width = it.size.width
+
+                            itemPositions[index] = TabItemPosition(
+                                (left / density).roundToInt().dp,
+                                (width / density).roundToInt().dp,
+                            )
+
+                            if (index == tabs.size - 1) updated = true
+                        }
+                        .clickable(
+                            indication = null,
+                            interactionSource = MutableInteractionSource(),
+                        ) {
+                            coroutineScope.launch {
+                                if (selectIndex == index) return@launch
+                                selectIndex = index
+                                onChange?.invoke(index)
+                                lazyListState.animateScrollToItem(
+                                    index,
+                                    (-lazyListWidth / 2 + itemPositions[index].width.value * density / 2).roundToInt()
+                                )
+                            }
+                        },
+                )
+            }
+        }
+        Box(modifier = Modifier.height(4.dp))
+        Box(
+            modifier = Modifier
+                .wrapContentSize()
+                .absoluteOffset(x = indicatorPosition - 6.dp)
+        ) {
+            Box(
+                modifier = Modifier
+                    .drawWithContent {
+                        if (lastPosition.value != 0f) {
+                            drawContent()
+                            val radius = size.width / 2 + size.height
+
+                            drawArc(
+                                color = Color.Blue,
+                                startAngle = 60f,
+                                sweepAngle = 60f,
+                                useCenter = false,
+                                topLeft = Offset(
+                                    x = size.width / 2 - radius,
+                                    y = -radius * 2,
+                                ),
+                                size = Size(
+                                    width = radius * 2,
+                                    height = radius * 2,
+                                ),
+                                style = Stroke(
+                                    width = 4.dp.toPx(),
+                                    cap = StrokeCap.Round,
+                                ),
+                            )
+                        }
+                    }
+                    .width(12.dp)
+                    .height(6.dp)
+            )
+        }
+    }
+
+    LaunchedEffect(initial) {
+        selectIndex = initial
+    }
+}
+
+@Composable
+private fun TabBarItem(
+    title: String,
+    modifier: Modifier = Modifier,
+    style: TextStyle,
+) {
+    Text(
+        text = title,
+        style = style,
+        modifier = modifier.wrapContentSize(),
+    )
+}

BIN
feat/home/src/main/res/mipmap-xhdpi/study_plan_light_off.png


BIN
feat/home/src/main/res/mipmap-xhdpi/study_plan_light_on.png


BIN
feat/home/src/main/res/mipmap-xhdpi/study_plan_lock.png


BIN
feat/home/src/main/res/mipmap-xhdpi/study_plan_lock_white.png


BIN
feat/home/src/main/res/mipmap-xhdpi/study_plan_no_item.png


BIN
feat/home/src/main/res/mipmap-xxhdpi/study_plan_light_off.png


BIN
feat/home/src/main/res/mipmap-xxhdpi/study_plan_light_on.png


BIN
feat/home/src/main/res/mipmap-xxhdpi/study_plan_lock.png


BIN
feat/home/src/main/res/mipmap-xxhdpi/study_plan_lock_white.png


BIN
feat/home/src/main/res/mipmap-xxhdpi/study_plan_no_item.png


BIN
feat/home/src/main/res/mipmap-xxxhdpi/study_plan_light_off.png


BIN
feat/home/src/main/res/mipmap-xxxhdpi/study_plan_light_on.png


BIN
feat/home/src/main/res/mipmap-xxxhdpi/study_plan_lock.png


BIN
feat/home/src/main/res/mipmap-xxxhdpi/study_plan_lock_white.png


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff