From a7820e2f1ee06a7b43b4d351cced3343d7e1a5e2 Mon Sep 17 00:00:00 2001
From: cloudroam <cloudroam>
Date: 星期一, 31 三月 2025 08:55:52 +0800
Subject: [PATCH] fix 登录限制

---
 app/src/main/res/values-v23/themes.xml                                          |   12 
 app/src/main/java/com/example/firstapp/ui/login/LoginViewModel.kt               |   54 ++
 app/src/main/res/values/themes.xml                                              |    6 
 app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt                 |  170 +++++++++
 app/src/main/java/com/example/firstapp/MainActivity.kt                          |    3 
 app/src/main/java/com/example/firstapp/database/response/OAuth2TokenResponse.kt |   17 
 app/src/main/res/layout/item_category_selector.xml                              |   22 +
 app/build.gradle                                                                |    7 
 app/src/main/java/com/example/firstapp/adapter/CategorySelectorAdapter.kt       |   66 +++
 app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt                  |    3 
 app/src/main/AndroidManifest.xml                                                |   12 
 app/src/main/res/drawable/home_add.xml                                          |    9 
 app/src/main/java/com/example/firstapp/database/service/ApiService.kt           |   34 +
 app/src/main/java/com/example/firstapp/util/CategoryDragCallback.kt             |   25 +
 app/src/main/res/layout/fragment_home.xml                                       |  232 ++++++++----
 app/src/main/java/com/example/firstapp/model/CategoryConfigSync.kt              |    6 
 /dev/null                                                                       |   29 -
 app/src/main/java/com/example/firstapp/database/service/ModelService.kt         |   33 +
 app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt                  |  211 +++++++++--
 app/src/main/java/com/example/firstapp/util/SecureStorage.kt                    |   49 ++
 app/src/main/res/layout/dialog_category_selector.xml                            |   29 +
 app/src/main/java/com/example/firstapp/model/CategoryConfig.kt                  |   11 
 app/src/main/java/com/example/firstapp/database/service/RetrofitClient.kt       |    1 
 app/src/main/java/com/example/firstapp/database/request/SmsLoginRequest.kt      |    7 
 app/src/main/java/com/example/firstapp/database/request/SmsSendRequest.kt       |    6 
 25 files changed, 853 insertions(+), 201 deletions(-)

diff --git a/app/build.gradle b/app/build.gradle
index 3257afb..6f29ef6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -283,7 +283,12 @@
     // 支付宝支付SDK
     api 'com.alipay.sdk:alipaysdk-android:+@aar'
 
-    implementation 'com.google.android.material:material:1.4.0'
+//    implementation 'com.google.android.material:material:1.4.0'
 
+    // 加密SharedPreferences
+    implementation "androidx.security:security-crypto:1.1.0-alpha06"
+    
+    // Material Design组件
+    implementation 'com.google.android.material:material:1.9.0'
 
 }
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d826628..0f78127 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -84,12 +84,12 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.FirstApp"
         tools:targetApi="31">
-        <activity
-            android:name=".ui.reminderOther.ReminderOtherAddActivity2"
-            android:configChanges="orientation|keyboardHidden|screenSize"
-            android:exported="false"
-            android:label="@string/title_activity_reminder_other_add2"
-            android:theme="@style/Theme.FirstApp.Fullscreen" />
+<!--        <activity-->
+<!--            android:name=".ui.reminderOther.ReminderOtherAddActivity2"-->
+<!--            android:configChanges="orientation|keyboardHidden|screenSize"-->
+<!--            android:exported="false"-->
+<!--            android:label="@string/title_activity_reminder_other_add2"-->
+<!--            android:theme="@style/Theme.FirstApp.Fullscreen" />-->
         <activity
             android:name=".activity.LoginActivity"
             android:exported="true"
diff --git a/app/src/main/java/com/example/firstapp/MainActivity.kt b/app/src/main/java/com/example/firstapp/MainActivity.kt
index 8b689c9..0554ff5 100644
--- a/app/src/main/java/com/example/firstapp/MainActivity.kt
+++ b/app/src/main/java/com/example/firstapp/MainActivity.kt
@@ -26,6 +26,7 @@
 import com.example.firstapp.database.entity.Code
 import com.example.firstapp.database.entity.Msg
 import com.example.firstapp.database.service.RetrofitClient
+import com.example.firstapp.database.service.RetrofitModelClient
 import com.example.firstapp.ui.home.HomeViewModel
 import com.example.firstapp.utils.Log
 import com.example.firstapp.workers.KeywordUpdateWorker
@@ -256,7 +257,7 @@
                     CoroutineScope(Dispatchers.IO).launch {
                         try {
                             // API调用移到synchronized块外
-                            val response = RetrofitClient.apiService.processSms(mapOf("content" to messageBody))
+                            val response = RetrofitModelClient.modelService.processSms(mapOf("content" to messageBody))
                             
                             // 数据库操作放在synchronized块内
                             synchronized(syncLock) {
diff --git a/app/src/main/java/com/example/firstapp/adapter/CategorySelectorAdapter.kt b/app/src/main/java/com/example/firstapp/adapter/CategorySelectorAdapter.kt
new file mode 100644
index 0000000..f3c46b9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/adapter/CategorySelectorAdapter.kt
@@ -0,0 +1,66 @@
+package com.example.firstapp.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.databinding.ItemCategorySelectorBinding
+import com.example.firstapp.model.CategoryConfig
+import java.util.Collections
+
+/**
+ * Adapter for the category selector recycler view.分类选择的适配器
+ */
+class CategorySelectorAdapter : RecyclerView.Adapter<CategorySelectorAdapter.ViewHolder>() {
+    
+    private var categories = mutableListOf<CategoryConfig>()
+    private var lastCheckedPosition = -1 // 记录最后一个选中的位置
+
+    class ViewHolder(val binding: ItemCategorySelectorBinding) : RecyclerView.ViewHolder(binding.root)
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val binding = ItemCategorySelectorBinding.inflate(
+            LayoutInflater.from(parent.context), parent, false
+        )
+        return ViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val category = categories[position]
+        holder.binding.apply {
+            categoryName.text = category.name
+            categoryCheckBox.isChecked = category.isEnabled
+            
+            // 如果只有一个选中的项,禁用其复选框
+            categoryCheckBox.isEnabled = !(getEnabledCount() == 1 && category.isEnabled)
+            
+            categoryCheckBox.setOnCheckedChangeListener { _, isChecked ->
+                if (isChecked || getEnabledCount() > 1) {
+                    categories[position] = category.copy(isEnabled = isChecked)
+                    notifyDataSetChanged() // 刷新所有项以更新禁用状态
+                } else {
+                    // 如果要取消选中且只有一个选中项,恢复选中状态
+                    categoryCheckBox.isChecked = true
+                }
+            }
+        }
+    }
+
+    private fun getEnabledCount(): Int {
+        return categories.count { it.isEnabled }
+    }
+
+    override fun getItemCount() = categories.size
+
+    fun moveItem(fromPosition: Int, toPosition: Int) {
+        Collections.swap(categories, fromPosition, toPosition)
+        notifyItemMoved(fromPosition, toPosition)
+    }
+
+    fun setCategories(newCategories: List<CategoryConfig>) {
+        categories.clear()
+        categories.addAll(newCategories)
+        notifyDataSetChanged()
+    }
+
+    fun getCategories() = categories.toList()
+} 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/request/SmsLoginRequest.kt b/app/src/main/java/com/example/firstapp/database/request/SmsLoginRequest.kt
new file mode 100644
index 0000000..d76dce6
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/request/SmsLoginRequest.kt
@@ -0,0 +1,7 @@
+package com.example.firstapp.database.request
+
+data class SmsLoginRequest(
+    val username: String,
+    val smsCode: String,
+    val userType: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/request/SmsSendRequest.kt b/app/src/main/java/com/example/firstapp/database/request/SmsSendRequest.kt
new file mode 100644
index 0000000..ea46ff7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/request/SmsSendRequest.kt
@@ -0,0 +1,6 @@
+package com.example.firstapp.database.request
+
+data class SmsSendRequest(
+    val tel: String,
+    val userType: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/response/OAuth2TokenResponse.kt b/app/src/main/java/com/example/firstapp/database/response/OAuth2TokenResponse.kt
new file mode 100644
index 0000000..18ea0b5
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/response/OAuth2TokenResponse.kt
@@ -0,0 +1,17 @@
+import com.google.gson.annotations.SerializedName
+
+data class OAuth2AccessToken(
+    @SerializedName("access_token")
+    val value: String,          // token值
+    @SerializedName("token_type")
+    val tokenType: String,      // token类型
+    @SerializedName("refresh_token")
+    val refreshToken: String,   // 刷新token
+    val scope: String          // 作用域
+)
+
+data class TokenResponse(
+    val code: String,          // 注意这里改为 String 类型
+    val msg: String,
+    val data: OAuth2AccessToken?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/service/ApiService.kt b/app/src/main/java/com/example/firstapp/database/service/ApiService.kt
index 3171ff6..0e52267 100644
--- a/app/src/main/java/com/example/firstapp/database/service/ApiService.kt
+++ b/app/src/main/java/com/example/firstapp/database/service/ApiService.kt
@@ -1,25 +1,29 @@
 package com.example.firstapp.database.service
 
+import TokenResponse
 import com.example.firstapp.database.entity.ApiResponse
 import com.example.firstapp.database.entity.KeywordConfig
+import com.example.firstapp.database.request.SmsLoginRequest
+import com.example.firstapp.database.request.SmsSendRequest
 import com.example.firstapp.database.response.AlipayOrderInfoResponse
 import com.example.firstapp.database.response.ContentResponse
 import com.example.firstapp.database.response.DictResponse
 import com.example.firstapp.database.response.LoginResponse
 import com.example.firstapp.database.response.SecurityResponse
-import com.example.firstapp.database.response.SmsProcessResponse
 import com.example.firstapp.database.response.UserInfo
+import com.example.firstapp.model.CategoryConfig
+import com.example.firstapp.model.CategoryConfigSync
 import okhttp3.MultipartBody
 import okhttp3.RequestBody
 import retrofit2.Retrofit
 import retrofit2.converter.gson.GsonConverterFactory
-import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.Multipart
 import retrofit2.http.POST
 import retrofit2.http.Part
 import retrofit2.http.Path
 import retrofit2.http.Query
+import retrofit2.http.Body
 
 /**
  * API调用接口
@@ -35,11 +39,11 @@
     @GET("sysDict/getByDictCodeAndItemText")
     suspend fun getDictValue(@Query("dictCode") dictCode: String, @Query("itemText") itemText: String): DictResponse
 
-    @POST("sms/send-code")
-    suspend fun sendVerificationCode(@Query("phone") phone: String): LoginResponse
+    @POST("api/sms/send/code")
+    suspend fun sendVerificationCode(@Body request: SmsSendRequest): LoginResponse
 
-    @POST("sms/login")
-    suspend fun verifyCode(@Query("phone") phone: String, @Query("code") code: String): LoginResponse
+    @POST("api/login/customer/phone/v2")
+    suspend fun verifyCode(@Body request: SmsLoginRequest): TokenResponse
 
     @GET("config-security/enable-list-all")
     suspend fun getSecurityList(): SecurityResponse
@@ -57,20 +61,26 @@
         @Part avatar: MultipartBody.Part?
     ): ApiResponse<Unit>
 
-    @POST("process-sms")
-    suspend fun processSms(@Body body: Map<String, String>): SmsProcessResponse
+
+    fun getUserCategories(currentUserId: String): List<CategoryConfig>
+
+    fun saveUserCategories(categoryConfigSync: CategoryConfigSync)
+
 }
 
 // 创建Retrofit实例(单例)
 object RetrofitClient{
 
-//    private const val BASE_URL ="http://192.168.1.213:8888/jshERP-boot/"
-    private const val BASE_URL ="http://192.168.1.213:5000/"
+    private const val BASE_URL ="http://192.168.1.213:8080/flower/"
 
     //添加Gson解析器,用于自动将JSON响应转换为Kotlin/Java对象
-    private val retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build()
+    private val retrofit = Retrofit
+        .Builder()
+        .baseUrl(BASE_URL)
+        .addConverterFactory(GsonConverterFactory.create())
+        .build()
 
     //通过动态代理技术创建ApiService接口的具体实现类
     val apiService:ApiService = retrofit.create(ApiService::class.java)
 
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/firstapp/database/service/ModelService.kt b/app/src/main/java/com/example/firstapp/database/service/ModelService.kt
new file mode 100644
index 0000000..27bfbd9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/service/ModelService.kt
@@ -0,0 +1,33 @@
+package com.example.firstapp.database.service
+
+import com.example.firstapp.database.response.SmsProcessResponse
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+/**
+ * 模型相关接口
+ */
+interface ModelService {
+
+    @POST("process-sms")
+    suspend fun processSms(@Body body: Map<String, String>): SmsProcessResponse
+}
+
+// 创建Retrofit实例(单例)
+object RetrofitModelClient {
+
+    private const val BASE_URL = "http://192.168.1.213:5000/"
+
+    //添加Gson解析器,用于自动将JSON响应转换为Kotlin/Java对象
+    private val retrofit = Retrofit
+        .Builder()
+        .baseUrl(BASE_URL)
+        .addConverterFactory(GsonConverterFactory.create())
+        .build()
+
+    //通过动态代理技术创建ApiService接口的具体实现类
+    val modelService: ModelService = retrofit.create(ModelService::class.java)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/service/RetrofitClient.kt b/app/src/main/java/com/example/firstapp/database/service/RetrofitClient.kt
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/service/RetrofitClient.kt
@@ -0,0 +1 @@
+ 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/model/CategoryConfig.kt b/app/src/main/java/com/example/firstapp/model/CategoryConfig.kt
new file mode 100644
index 0000000..29a37e9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/model/CategoryConfig.kt
@@ -0,0 +1,11 @@
+package com.example.firstapp.model
+
+/**
+ * Created by fanghaowei on 2025/03/27. 首页数据配置类
+ */
+data class CategoryConfig(
+    val id: Int,
+    val name: String,
+    val order: Int,
+    val isEnabled: Boolean = true
+) 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/model/CategoryConfigSync.kt b/app/src/main/java/com/example/firstapp/model/CategoryConfigSync.kt
new file mode 100644
index 0000000..82bf6d0
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/model/CategoryConfigSync.kt
@@ -0,0 +1,6 @@
+package com.example.firstapp.model
+
+data class CategoryConfigSync(
+    val userId: String,
+    val categories: List<CategoryConfig>
+) 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt
index 3d732b4..9a611bb 100644
--- a/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt
+++ b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt
@@ -13,6 +13,7 @@
 import com.example.firstapp.database.entity.Code
 import com.example.firstapp.database.entity.Msg
 import com.example.firstapp.database.service.RetrofitClient
+import com.example.firstapp.database.service.RetrofitModelClient
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -58,7 +59,7 @@
                 CoroutineScope(Dispatchers.IO).launch {
                     try {
                         val response =
-                            RetrofitClient.apiService.processSms(mapOf("content" to messageBody.toString()))
+                            RetrofitModelClient.modelService.processSms(mapOf("content" to messageBody.toString()))
 
                         if (response.status == "success") {
                             // 获取当前时间
diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt
index 230fb9c..481756d 100644
--- a/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt
@@ -8,6 +8,8 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
 import androidx.core.content.ContextCompat
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
@@ -17,7 +19,10 @@
 import com.example.firstapp.activity.PickupActivity
 import com.example.firstapp.adapter.ExpressAdapter
 import com.example.firstapp.adapter.FinanceAdapter
+import com.example.firstapp.adapter.CategorySelectorAdapter
 import com.example.firstapp.databinding.FragmentHomeBinding
+import com.example.firstapp.databinding.DialogCategorySelectorBinding
+import com.google.android.material.bottomsheet.BottomSheetDialog
 
 class HomeFragment : Fragment() {
 
@@ -30,7 +35,9 @@
     private lateinit var homeViewModel: HomeViewModel
     private lateinit var expressAdapter: ExpressAdapter
     private lateinit var financeAdapter: FinanceAdapter
-//    private lateinit var memorialAdapter: MemorialAdapter
+    private lateinit var incomeAdapter: FinanceAdapter
+    private lateinit var flightAdapter: FinanceAdapter
+    private lateinit var trainAdapter: FinanceAdapter
     private lateinit var dataUpdateReceiver: BroadcastReceiver
 
     //onCreateView这个方法创建后被调用,通常是初始化视图组件和观察者
@@ -46,15 +53,18 @@
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        //通过 ViewModelProvider 获取 HomeViewModel 的实例,以便在视图中使用。
         homeViewModel = ViewModelProvider(this).get(HomeViewModel::class.java)
-        // 加载广告图片
-        //loadAdvertisements()
+        // 假设从某处获取用户ID
+//        val userId = getUserId() // 需要实现这个方法
+        val userId ="123456"
+        homeViewModel.initialize(requireContext(), userId)
+        
         //调用这个方法来设置 RecyclerView用于设置 RecyclerView 的布局和适配器。
         setupRecyclerViews()
         setupTabSwitching()
         //调用这个方法来观察 ViewModel 中的数据变化
         observeViewModelData()
+        setupCategorySelector()
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -107,13 +117,28 @@
                 startActivity(intent)
             }
         }
-//
-//        // 纪念日列表
-//        binding.memorialRecycler.apply {
-//            layoutManager = LinearLayoutManager(context)
-//            memorialAdapter = MemorialAdapter()
-//            adapter = memorialAdapter
-//        }
+
+        // 添加新的 RecyclerView
+        binding.incomeRecycler.apply {
+            layoutManager = LinearLayoutManager(context)
+            incomeAdapter = FinanceAdapter()
+            adapter = incomeAdapter
+            visibility = View.GONE
+        }
+
+        binding.flightRecycler.apply {
+            layoutManager = LinearLayoutManager(context)
+            flightAdapter = FinanceAdapter()
+            adapter = flightAdapter
+            visibility = View.GONE
+        }
+
+        binding.trainRecycler.apply {
+            layoutManager = LinearLayoutManager(context)
+            trainAdapter = FinanceAdapter()
+            adapter = trainAdapter
+            visibility = View.GONE
+        }
     }
 
     private fun setupTabSwitching() {
@@ -121,45 +146,63 @@
             // 设置初始状态
             tabExpress.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_selected))
             tabFinance.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-            others.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-            
+
             // 快递标签点击事件
             tabExpress.setOnClickListener {
+                hideAllRecyclers()
                 expressRecycler.visibility = View.VISIBLE
-                financeRecycler.visibility = View.GONE
-                tabExpress.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_selected))
-                tabFinance.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                others.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                tabExpress.textSize = 16f
-                tabFinance.textSize = 14f
-                others.textSize = 14f
+                updateTabStyles(tabExpress)
+                homeViewModel.loadExpressData()
             }
             
             // 财务标签点击事件
             tabFinance.setOnClickListener {
-                expressRecycler.visibility = View.GONE
+                hideAllRecyclers()
                 financeRecycler.visibility = View.VISIBLE
-                tabExpress.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                tabFinance.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_selected))
-                others.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                tabExpress.textSize = 14f
-                tabFinance.textSize = 16f
-                others.textSize = 14f
-
-                // 在切换到财务标签时加载数据 - 添加这行
+                updateTabStyles(tabFinance)
                 homeViewModel.loadFinanceData()
             }
 
-            // 其他标签点击事件
-            others.setOnClickListener {
-                expressRecycler.visibility = View.GONE
-                financeRecycler.visibility = View.GONE
-                tabExpress.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                tabFinance.setTextColor(ContextCompat.getColor(requireContext(), R.color.gray))
-                others.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_selected))
-                tabExpress.textSize = 14f
-                tabFinance.textSize = 14f
-                others.textSize = 16f
+            tabIncome.setOnClickListener {
+                hideAllRecyclers()
+                incomeRecycler.visibility = View.VISIBLE
+                updateTabStyles(tabIncome)
+                homeViewModel.loadIncomeData()
+            }
+
+            tabFlight.setOnClickListener {
+                hideAllRecyclers()
+                flightRecycler.visibility = View.VISIBLE
+                updateTabStyles(tabFlight)
+                homeViewModel.loadFlightData()
+            }
+
+            tabTrain.setOnClickListener {
+                hideAllRecyclers()
+                trainRecycler.visibility = View.VISIBLE
+                updateTabStyles(tabTrain)
+                homeViewModel.loadTrainData()
+            }
+        }
+    }
+
+    private fun hideAllRecyclers() {
+        binding.apply {
+            expressRecycler.visibility = View.GONE
+            financeRecycler.visibility = View.GONE
+            incomeRecycler.visibility = View.GONE
+            flightRecycler.visibility = View.GONE
+            trainRecycler.visibility = View.GONE
+        }
+    }
+
+    private fun updateTabStyles(selectedTab: TextView) {
+        binding.apply {
+            val tabs = listOf(tabExpress, tabFinance, tabIncome, tabFlight, tabTrain)
+            tabs.forEach { tab ->
+                tab.setTextColor(ContextCompat.getColor(requireContext(), 
+                    if (tab == selectedTab) R.color.tab_selected else R.color.gray))
+                tab.textSize = if (tab == selectedTab) 16f else 14f
             }
         }
     }
@@ -175,10 +218,56 @@
         homeViewModel.financeItems.observe(viewLifecycleOwner) { items ->
             financeAdapter.submitList(items)
         }
-//
-//        homeViewModel.memorialItems.observe(viewLifecycleOwner) { items ->
-//            memorialAdapter.submitList(items)
-//        }
+
+        homeViewModel.incomeItems.observe(viewLifecycleOwner) { items ->
+            incomeAdapter.submitList(items)
+        }
+
+        homeViewModel.flightItems.observe(viewLifecycleOwner) { items ->
+            flightAdapter.submitList(items)
+        }
+
+        homeViewModel.trainItems.observe(viewLifecycleOwner) { items ->
+            trainAdapter.submitList(items)
+        }
+
+        // 观察可见分类的变化
+        homeViewModel.visibleCategories.observe(viewLifecycleOwner) { categories: List<String> ->
+            binding.apply {
+                // 隐藏所有标签
+                tabExpress.visibility = View.GONE
+                tabFinance.visibility = View.GONE
+                tabIncome.visibility = View.GONE
+                tabFlight.visibility = View.GONE
+                tabTrain.visibility = View.GONE
+
+                // 根据选中的分类显示对应的标签
+                categories.forEachIndexed { index: Int, categoryName: String ->
+                    when (categoryName) {
+                        "快递" -> {
+                            tabExpress.visibility = View.VISIBLE
+                            if (index == 0) tabExpress.performClick()
+                        }
+                        "还款" -> {
+                            tabFinance.visibility = View.VISIBLE
+                            if (index == 0) tabFinance.performClick()
+                        }
+                        "收入" -> {
+                            tabIncome.visibility = View.VISIBLE
+                            if (index == 0) tabIncome.performClick()
+                        }
+                        "航班" -> {
+                            tabFlight.visibility = View.VISIBLE
+                            if (index == 0) tabFlight.performClick()
+                        }
+                        "火车票" -> {
+                            tabTrain.visibility = View.VISIBLE
+                            if (index == 0) tabTrain.performClick()
+                        }
+                    }
+                }
+            }
+        }
     }
 
     override fun onResume() {
@@ -220,4 +309,40 @@
             .load("http://192.168.1.235:9999/advertisement/down.png")
             .into(binding.bottomAdBanner)
     }
+
+    private fun setupCategorySelector() {
+        binding.categoryButton.setOnClickListener {
+            // TODO: 检查会员状态
+            if (true) { // 临时设置为true,实际应该检查会员状态
+                showCategorySelectorDialog()
+            } else {
+                // 显示会员提示
+                Toast.makeText(requireContext(), "该功能仅对会员开放", Toast.LENGTH_SHORT).show()
+            }
+        }
+    }
+
+    private fun showCategorySelectorDialog() {
+        val dialog = BottomSheetDialog(requireContext())
+        val dialogBinding = DialogCategorySelectorBinding.inflate(layoutInflater)
+        dialog.setContentView(dialogBinding.root)
+
+        val adapter = CategorySelectorAdapter()
+        dialogBinding.categoryRecyclerView.apply {
+            layoutManager = LinearLayoutManager(context)
+            this.adapter = adapter
+        }
+
+        // 加载现有分类
+        homeViewModel.categories.observe(viewLifecycleOwner) { categories ->
+            adapter.setCategories(categories)
+        }
+
+        dialogBinding.saveButton.setOnClickListener {
+            homeViewModel.saveCategories(adapter.getCategories())
+            dialog.dismiss()
+        }
+
+        dialog.show()
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
index 7b5c3fb..d8c92f2 100644
--- a/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
@@ -1,29 +1,59 @@
 package com.example.firstapp.ui.home
 
+import android.content.Context
+import android.util.Log
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.example.firstapp.core.Core
-import com.example.firstapp.database.entity.Code
+import com.example.firstapp.database.service.RetrofitClient
+import com.example.firstapp.model.CategoryConfig
+import com.example.firstapp.model.CategoryConfigSync
 import com.example.firstapp.model.ExpressGroup
 import com.example.firstapp.model.ExpressPackage
 import com.example.firstapp.model.FinanceGroup
 import com.example.firstapp.model.FinancePackage
+import com.example.firstapp.util.SecureStorage
 import kotlinx.coroutines.launch
 
 class HomeViewModel : ViewModel() {
 
     private val _expressItems = MutableLiveData<List<ExpressGroup>>()
     private val _financeItems = MutableLiveData<List<FinanceGroup>>()
+    private val _incomeItems = MutableLiveData<List<FinanceGroup>>()
+    private val _flightItems = MutableLiveData<List<FinanceGroup>>()
+    private val _trainItems = MutableLiveData<List<FinanceGroup>>()
+    
     val expressItems: LiveData<List<ExpressGroup>> = _expressItems
     val financeItems: LiveData<List<FinanceGroup>> = _financeItems
+    val incomeItems: LiveData<List<FinanceGroup>> = _incomeItems
+    val flightItems: LiveData<List<FinanceGroup>> = _flightItems
+    val trainItems: LiveData<List<FinanceGroup>> = _trainItems
+
+    private val _categories = MutableLiveData<List<CategoryConfig>>()
+    val categories: LiveData<List<CategoryConfig>> = _categories
+
+    // 添加可见分类的 LiveData
+    private val _visibleCategories = MutableLiveData<List<String>>()
+    val visibleCategories: LiveData<List<String>> = _visibleCategories
+
+    private lateinit var secureStorage: SecureStorage
+    private lateinit var currentUserId: String
 
     init {
         // 初始化时加载包裹列表数据
         loadExpressData()
         // 初始化时不加载财务列表数据 0317
         //  loadFinanceData()
+    }
+
+    fun initialize(context: Context, userId: String) {
+        secureStorage = SecureStorage(context)
+        currentUserId = userId
+        loadCategories()
+        // 初始化时更新可见分类
+        _categories.value?.let { updateVisibleCategories(it) }
     }
 
     fun loadExpressData() {
@@ -74,4 +104,142 @@
         }
     }
 
+    fun loadIncomeData() {
+        viewModelScope.launch {
+            val stations = Core.reminder.getByType("收入")
+            val groups = stations.map { station ->
+                val packages = Core.code.getByKeyword(station.nickname).map { code ->
+                    FinancePackage(
+                        id = code.id,
+                        company = code.secondLevel,
+                        trackingNumber = code.code,
+                        createTime = code.createTime
+                    )
+                }
+                FinanceGroup(stationName = station.nickname, packages = packages)
+            }
+            _incomeItems.postValue(groups)
+        }
+    }
+
+    fun loadFlightData() {
+        viewModelScope.launch {
+            val stations = Core.reminder.getByType("航班")
+            val groups = stations.map { station ->
+                val packages = Core.code.getByKeyword(station.nickname).map { code ->
+                    FinancePackage(
+                        id = code.id,
+                        company = code.secondLevel,
+                        trackingNumber = code.code,
+                        createTime = code.createTime
+                    )
+                }
+                FinanceGroup(stationName = station.nickname, packages = packages)
+            }
+            _flightItems.postValue(groups)
+        }
+    }
+
+    fun loadTrainData() {
+        viewModelScope.launch {
+            val stations = Core.reminder.getByType("火车票")
+            val groups = stations.map { station ->
+                val packages = Core.code.getByKeyword(station.nickname).map { code ->
+                    FinancePackage(
+                        id = code.id,
+                        company = code.secondLevel,
+                        trackingNumber = code.code,
+                        createTime = code.createTime
+                    )
+                }
+                FinanceGroup(stationName = station.nickname, packages = packages)
+            }
+            _trainItems.postValue(groups)
+        }
+    }
+
+    fun loadCategories() {
+        viewModelScope.launch {
+            try {
+                // 先尝试从服务器获取配置
+                val serverCategories = RetrofitClient.apiService.getUserCategories(currentUserId)
+                if (serverCategories.isNotEmpty()) {
+                    _categories.value = serverCategories
+                    secureStorage.saveCategories(currentUserId, serverCategories)
+                } else {
+                    // 如果服务器没有配置,尝试获取本地配置
+                    val localCategories = secureStorage.getCategories(currentUserId)
+                    if (localCategories.isEmpty()) {
+                        // 如果本地也没有配置,使用默认配置
+                        val defaultCategories = listOf(
+                            CategoryConfig(1, "快递", 0),
+                            CategoryConfig(2, "还款", 1),
+                            CategoryConfig(3, "收入", 2),
+                            CategoryConfig(4, "航班", 3),
+                            CategoryConfig(5, "火车票", 4)
+                        )
+                        _categories.value = defaultCategories
+                        syncCategoriesToServer(defaultCategories)
+                    } else {
+                        _categories.value = localCategories
+                        syncCategoriesToServer(localCategories)
+                    }
+                }
+            } catch (e: Exception) {
+                // 如果网络请求失败,使用本地数据
+                val localCategories = secureStorage.getCategories(currentUserId)
+                _categories.value = localCategories.ifEmpty {
+                    listOf(
+                        CategoryConfig(1, "快递", 0),
+                        CategoryConfig(2, "还款", 1),
+                        CategoryConfig(3, "收入", 2),
+                        CategoryConfig(4, "航班", 3),
+                        CategoryConfig(5, "火车票", 4)
+                    )
+                }
+            }
+        }
+    }
+
+    private fun syncCategoriesToServer(categories: List<CategoryConfig>) {
+        viewModelScope.launch {
+            try {
+                RetrofitClient.apiService.saveUserCategories(
+                    CategoryConfigSync(currentUserId, categories)
+                )
+            } catch (e: Exception) {
+                // 同步失败,可以稍后重试或者显示提示
+                Log.e("CategorySync", "Failed to sync categories: ${e.message}")
+            }
+        }
+    }
+
+    fun saveCategories(categories: List<CategoryConfig>) {
+        viewModelScope.launch {
+            // 保存到本地
+            secureStorage.saveCategories(currentUserId, categories)
+            // 同步到服务器
+            syncCategoriesToServer(categories)
+            _categories.value = categories
+            
+            // 更新可见分类
+            updateVisibleCategories(categories)
+        }
+    }
+
+    private fun updateVisibleCategories(categories: List<CategoryConfig>) {
+        val visibleNames = categories
+            .filter { it.isEnabled }
+            .sortedBy { it.order }
+            .map { it.name }
+        
+        _visibleCategories.value = visibleNames
+    }
+
+    // 登出时不再清除本地数据
+    fun logout() {
+        // 只清除内存中的数据
+        _categories.value = emptyList()
+    }
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/firstapp/ui/login/LoginViewModel.kt
index f61b646..ee242e2 100644
--- a/app/src/main/java/com/example/firstapp/ui/login/LoginViewModel.kt
+++ b/app/src/main/java/com/example/firstapp/ui/login/LoginViewModel.kt
@@ -5,8 +5,11 @@
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.launch
 import androidx.lifecycle.ViewModel
+import com.example.firstapp.database.request.SmsLoginRequest
+import com.example.firstapp.database.request.SmsSendRequest
 import com.example.firstapp.database.service.RetrofitClient
 import com.example.firstapp.utils.Log
+import com.example.firstapp.ui.home.HomeViewModel
 
 
 class LoginViewModel : ViewModel() {
@@ -19,16 +22,26 @@
     private val _isLoading = MutableLiveData<Boolean>()
     val isLoading: LiveData<Boolean> = _isLoading
 
+    private lateinit var homeViewModel: HomeViewModel
+
     fun sendVerificationCode(phone: String) {
         viewModelScope.launch {
             _isLoading.value = true
             try {
-//                val response = RetrofitClient.apiService.sendVerificationCode(phone)
-//                if (response.code == 200) {
+                // 创建 SmsSendRequest 对象
+                val request = SmsSendRequest(
+                    tel = phone,
+                    userType = "customer"
+                )
+                //Retrofit 进行网络请求时,类名不需要完全一致,只要保证类的属性名称和类型与后端 DTO 对象的属性一致即可。
+                //Retrofit + Gson 在序列化时会将对象转换为 JSON,后端 Spring 框架会将 JSON 反序列化为 SmsSendDTO 对象
+                //HTTP 请求实际传输的是 JSON 格式的数据,而不是 Java/Kotlin 对象。
+                val response = RetrofitClient.apiService.sendVerificationCode(request)
+                if (response.code == 0) {
                     _loginMessage.value = "验证码已发送"
-//                } else {
-//                    _loginMessage.value = response.msg.ifEmpty { "发送验证码失败" }
-//                }
+                } else {
+                    _loginMessage.value = response.msg.ifEmpty { "发送验证码失败" }
+                }
             } catch (e: Exception) {
                 Log.e("LoginError", "Login failed: ${e.message}", e)
                 _loginMessage.value = "网络错误,请稍后重试"
@@ -42,12 +55,20 @@
         viewModelScope.launch {
             _isLoading.value = true
             try {
-//                val response = RetrofitClient.apiService.verifyCode(phone, code)
-//                if (response.code == 200 && response.data) {
+                val request = SmsLoginRequest(
+                    username = phone,
+                    smsCode = code,
+                    userType = "customer"
+                )
+                //HttpServletRequest request这是后端 Spring 框架中的一个特殊参数,
+                //用于获取 HTTP 请求的相关信息(如请求头、Cookie 等),它会由 Spring 框架自动注入,不需要客户端显式传递。
+                val response = RetrofitClient.apiService.verifyCode(request)
+                if (response.code == "0" && response.data != null) {
+                    saveToken(response.data.value)  // 这里获取的是 access_token
                     _loginState.value = true
-//                } else {
-//                    _loginMessage.value = response.msg.ifEmpty { "登录失败" }
-//                }
+                } else {
+                    _loginMessage.value = response.msg.ifEmpty { "登录失败" }
+                }
             } catch (e: Exception) {
                 Log.e("LoginError", "Login failed: ${e.message}", e)
                 _loginMessage.value = "网络错误,请稍后重试"
@@ -57,4 +78,17 @@
         }
     }
 
+    fun logout() {
+        viewModelScope.launch {
+            // 不再清除用户数据,只执行登出操作
+            homeViewModel.logout()
+            // 其他登出操作...
+        }
+    }
+
+    private fun saveToken(token: String) {
+        // TODO: 实现token存储逻辑
+        // 可能还需要存储 refresh_token
+    }
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/util/CategoryDragCallback.kt b/app/src/main/java/com/example/firstapp/util/CategoryDragCallback.kt
new file mode 100644
index 0000000..5c8c5ae
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/util/CategoryDragCallback.kt
@@ -0,0 +1,25 @@
+package com.example.firstapp.util
+
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.adapter.CategorySelectorAdapter
+
+/**
+ * 拖拽回调
+ */
+class CategoryDragCallback(private val adapter: CategorySelectorAdapter) : ItemTouchHelper.Callback() {
+    
+    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
+        val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
+        return makeMovementFlags(dragFlags, 0)
+    }
+
+    override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
+        adapter.moveItem(viewHolder.adapterPosition, target.adapterPosition)
+        return true
+    }
+
+    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+        // 不需要实现
+    }
+} 
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/util/SecureStorage.kt b/app/src/main/java/com/example/firstapp/util/SecureStorage.kt
new file mode 100644
index 0000000..ee656c7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/util/SecureStorage.kt
@@ -0,0 +1,49 @@
+package com.example.firstapp.util
+
+import android.content.Context
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
+import com.google.gson.Gson
+import com.example.firstapp.model.CategoryConfig
+
+class SecureStorage(context: Context) {
+    private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+    
+    private val sharedPreferences = EncryptedSharedPreferences.create(
+        "secure_prefs",
+        masterKeyAlias,
+        context,
+        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+    )
+
+    private val gson = Gson()
+
+    private fun getStorageKey(userId: String): String {
+        return "categories_$userId"
+    }
+
+    fun saveCategories(userId: String, categories: List<CategoryConfig>) {
+        val json = gson.toJson(categories)
+        sharedPreferences.edit().putString(getStorageKey(userId), json).apply()
+    }
+
+    fun getCategories(userId: String): List<CategoryConfig> {
+        val json = sharedPreferences.getString(getStorageKey(userId), null)
+        return if (json != null) {
+            gson.fromJson(json, Array<CategoryConfig>::class.java).toList()
+        } else {
+            emptyList()
+        }
+    }
+
+    // 清除指定用户的数据
+    fun clearUserData(userId: String) {
+        sharedPreferences.edit().remove(getStorageKey(userId)).apply()
+    }
+
+    // 清除所有数据
+    fun clearAllData() {
+        sharedPreferences.edit().clear().apply()
+    }
+} 
\ No newline at end of file
diff --git a/app/src/main/res/drawable/home_add.xml b/app/src/main/res/drawable/home_add.xml
new file mode 100644
index 0000000..7ece592
--- /dev/null
+++ b/app/src/main/res/drawable/home_add.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="1024"
+    android:viewportHeight="1024">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M991.7,915.6V90.9c0,-36.9 -29.9,-66.8 -66.8,-66.8H99.1c-36.9,0 -66.8,29.9 -66.8,66.8v824.7c0,36.9 29.9,66.8 66.8,66.8h825.7c36.9,0 66.8,-29.9 66.8,-66.8zM822.8,503.2c0,30.9 -25.1,56 -56,56H568v198.9c0,30.9 -25.1,56 -56,56s-56,-25.1 -56,-56V559.2H257.2c-30.9,0 -56,-25.1 -56,-56s25.1,-56 56,-56h198.9V248.4c0,-30.9 25.1,-56 56,-56s56,25.1 56,56v198.9h198.9c30.9,0 56,25.1 56,56z"/>
+</vector>
diff --git a/app/src/main/res/layout/dialog_category_selector.xml b/app/src/main/res/layout/dialog_category_selector.xml
new file mode 100644
index 0000000..6828364
--- /dev/null
+++ b/app/src/main/res/layout/dialog_category_selector.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="12dp">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="选择分类"
+        android:textSize="18sp"
+        android:textStyle="bold"
+        android:layout_marginBottom="8dp"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/categoryRecyclerView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+    <Button
+        android:id="@+id/saveButton"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="保存"
+        android:layout_marginTop="12dp"
+        android:backgroundTint="#000000"
+        android:textColor="#FFFFFF"/>
+</LinearLayout> 
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 18a5e11..17014a8 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -1,117 +1,173 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-<ScrollView
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:layout_marginTop="40dp"> <!-- 留出顶部广告位的高度 -->
-    >
-
-<!--    LinearLayout的作用是按照垂直或者水平方向排列其子视图-->
-<!--    CardView组件是用于实现卡片式布局-->
-<!--    RecyclerView 回收商视图 它使用适配器(Adapter)来管理数据的显示,-->
-<!--    开发者可以根据自己的需求实现适配器的方法,将数据与视图进行绑定。-->
-<!--    这使得 RecyclerView 能够轻松地处理各种类型的数据,并按照自定义的布局方式展示。-->
-<!--    支持局部刷新 通知数据集变化-->
-    <LinearLayout
+    <ScrollView
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
+        android:layout_height="match_parent"
+        android:layout_marginTop="40dp"> <!-- 留出顶部广告位的高度 -->
+        >
 
-
-
-        <!-- 快递/财务切换区域 -->
+        <!--    LinearLayout的作用是按照垂直或者水平方向排列其子视图-->
+        <!--    CardView组件是用于实现卡片式布局-->
+        <!--    RecyclerView 回收商视图 它使用适配器(Adapter)来管理数据的显示,-->
+        <!--    开发者可以根据自己的需求实现适配器的方法,将数据与视图进行绑定。-->
+        <!--    这使得 RecyclerView 能够轻松地处理各种类型的数据,并按照自定义的布局方式展示。-->
+        <!--    支持局部刷新 通知数据集变化-->
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:orientation="horizontal"
-            android:layout_marginBottom="8dp">
+            android:orientation="vertical">
 
-            <TextView
-                android:id="@+id/tabExpress"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="快递"
-                android:gravity="center"
-                android:padding="8dp"
-                android:textSize="16sp"
-                android:textStyle="bold"/>
 
-            <TextView
-                android:id="@+id/tabFinance"
-                android:layout_width="0dp"
+            <!-- 快递/财务切换区域 -->
+            <LinearLayout
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="财务"
-                android:gravity="center"
-                android:padding="8dp"
-                android:textSize="16sp"/>
+                android:layout_marginBottom="8dp"
+                android:layout_marginTop="8dp"
+                android:layout_marginHorizontal="8dp"
+                android:orientation="horizontal">
 
-            <TextView
-                android:id="@+id/others"
-                android:layout_width="0dp"
+                <TextView
+                    android:id="@+id/tabExpress"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center"
+                    android:padding="6dp"
+                    android:text="快递"
+                    android:textSize="14sp"
+                    android:textStyle="bold" />
+
+                <TextView
+                    android:id="@+id/tabFinance"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center"
+                    android:padding="6dp"
+                    android:text="还款"
+                    android:textSize="14sp" />
+
+                <TextView
+                    android:id="@+id/tabIncome"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center"
+                    android:padding="6dp"
+                    android:text="收入"
+                    android:textSize="14sp" />
+
+                <TextView
+                    android:id="@+id/tabFlight"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center"
+                    android:padding="6dp"
+                    android:text="航班"
+                    android:textSize="14sp" />
+
+                <TextView
+                    android:id="@+id/tabTrain"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:gravity="center"
+                    android:padding="6dp"
+                    android:text="火车票"
+                    android:textSize="14sp" />
+
+                <ImageButton
+                    android:id="@+id/categoryButton"
+                    android:layout_width="28dp"
+                    android:layout_height="28dp"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="2dp"
+                    android:layout_marginEnd="2dp"
+                    android:background="?attr/selectableItemBackgroundBorderless"
+                    android:contentDescription="分类设置"
+                    android:padding="4dp"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/home_add" />
+
+            </LinearLayout>
+
+            <!-- 内容区域 -->
+            <FrameLayout
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="其他"
-                android:gravity="center"
-                android:padding="8dp"
-                android:textSize="16sp"/>
+                android:layout_margin="16dp">
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/express_recycler"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="8dp" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/finance_recycler"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="8dp" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/income_recycler"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="8dp"
+                    android:visibility="gone" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/flight_recycler"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="8dp"
+                    android:visibility="gone" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/train_recycler"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="8dp"
+                    android:visibility="gone" />
+
+            </FrameLayout>
+
+            <!-- 底部广告位 -->
+            <androidx.cardview.widget.CardView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:cardCornerRadius="8dp"
+                app:cardElevation="2dp">
+
+                <ImageView
+                    android:id="@+id/bottomAdBanner"
+                    android:layout_width="match_parent"
+                    android:layout_height="80dp"
+                    android:scaleType="centerCrop" />
+            </androidx.cardview.widget.CardView>
+
         </LinearLayout>
-
-        <!-- 内容区域 -->
-        <FrameLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="16dp">
-
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/express_recycler"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:padding="8dp"/>
-
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/finance_recycler"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:padding="8dp"/>
-
-        </FrameLayout>
-
-        <!-- 底部广告位 -->
-        <androidx.cardview.widget.CardView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:cardCornerRadius="8dp"
-            app:cardElevation="2dp">
-            
-            <ImageView
-                android:id="@+id/bottomAdBanner"
-                android:layout_width="match_parent"
-                android:layout_height="80dp"
-                android:scaleType="centerCrop"/>
-        </androidx.cardview.widget.CardView>
-
-    </LinearLayout>
-</ScrollView>
+    </ScrollView>
 
     <!-- 顶部广告位 -->
     <androidx.cardview.widget.CardView
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-       >
+        android:layout_height="wrap_content">
 
         <ImageView
             android:id="@+id/adBanner"
             android:layout_width="match_parent"
             android:layout_height="40dp"
             android:scaleType="centerCrop"
-            android:src="@drawable/up"/>
+            android:src="@drawable/up" />
     </androidx.cardview.widget.CardView>
 
+    <!-- 在适当的位置添加 -->
+
 </FrameLayout>
diff --git a/app/src/main/res/layout/item_category_selector.xml b/app/src/main/res/layout/item_category_selector.xml
new file mode 100644
index 0000000..557638f
--- /dev/null
+++ b/app/src/main/res/layout/item_category_selector.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="8dp"
+    android:gravity="center_vertical">
+
+    <TextView
+        android:id="@+id/categoryName"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:textSize="16sp"/>
+
+    <CheckBox
+        android:id="@+id/categoryCheckBox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"/>
+
+</LinearLayout> 
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
deleted file mode 100644
index 4925b55..0000000
--- a/app/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<resources xmlns:tools="http://schemas.android.com/tools">
-    <!-- Base application theme. -->
-    <style name="Theme.FirstApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
-        <!-- Primary brand color. -->
-        <item name="colorPrimary">@color/purple_200</item>
-        <item name="colorPrimaryVariant">@color/purple_700</item>
-        <item name="colorOnPrimary">@color/black</item>
-        <!-- Secondary brand color. -->
-        <item name="colorSecondary">@color/teal_200</item>
-        <item name="colorSecondaryVariant">@color/teal_200</item>
-        <item name="colorOnSecondary">@color/black</item>
-        <!-- Status bar color. -->
-        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
-        <!-- 确保没有ActionBar -->
-        <item name="windowActionBar">false</item>
-        <item name="windowNoTitle">true</item>
-        <!-- Customize your theme here. -->
-    </style>
-
-    <style name="ThemeOverlay.FirstApp.FullscreenContainer" parent="">
-        <item name="fullscreenBackgroundColor">@color/light_blue_900</item>
-        <item name="fullscreenTextColor">@color/light_blue_A400</item>
-    </style>
-    <!-- Base application theme. -->
-    <style name="Base.Theme.FirstApp" parent="Theme.Material3.DayNight.NoActionBar">
-        <!-- Customize your dark theme here. -->
-        <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
-    </style>
-</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-v23/themes.xml b/app/src/main/res/values-v23/themes.xml
index b806642..940849b 100644
--- a/app/src/main/res/values-v23/themes.xml
+++ b/app/src/main/res/values-v23/themes.xml
@@ -1,9 +1,9 @@
 <resources xmlns:tools="http://schemas.android.com/tools">
 
-    <style name="Theme.FirstApp" parent="Base.Theme.FirstApp">
-        <!-- Transparent system bars for edge-to-edge. -->
-        <item name="android:navigationBarColor">@android:color/transparent</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-        <item name="android:windowLightStatusBar">?attr/isLightTheme</item>
-    </style>
+<!--    <style name="Theme.FirstApp" parent="Base.Theme.FirstApp">-->
+<!--        &lt;!&ndash; Transparent system bars for edge-to-edge. &ndash;&gt;-->
+<!--        <item name="android:navigationBarColor">@android:color/transparent</item>-->
+<!--        <item name="android:statusBarColor">@android:color/transparent</item>-->
+<!--        <item name="android:windowLightStatusBar">?attr/isLightTheme</item>-->
+<!--    </style>-->
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 02c10a9..74c31b4 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -19,9 +19,9 @@
     </style>
 
     <style name="Theme.FirstApp.Fullscreen" parent="Theme.FirstApp">
-        <item name="android:actionBarStyle">@style/Widget.Theme.FirstApp.ActionBar.Fullscreen</item>
-        <item name="android:windowActionBarOverlay">true</item>
-        <item name="android:windowBackground">@null</item>
+<!--        <item name="android:actionBarStyle">@style/Widget.Theme.FirstApp.ActionBar.Fullscreen</item>-->
+<!--        <item name="android:windowActionBarOverlay">true</item>-->
+<!--        <item name="android:windowBackground">@null</item>-->
     </style>
 
     <style name="ThemeOverlay.FirstApp.FullscreenContainer" parent="">

--
Gitblit v1.9.3