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