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">-->
+<!-- <!– 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>-->
</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