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' } 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" 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) { 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() } 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, ) 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 ) 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? ) 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) } } 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) } app/src/main/java/com/example/firstapp/database/service/RetrofitClient.kt
对比新文件 @@ -0,0 +1 @@ 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 ) 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> ) 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") { // 获取当前时间 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() } } 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() } } 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 } } 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) { // 不需要实现 } } 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() } } 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> 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> 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> 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> app/src/main/res/values-night/themes.xml
文件已删除 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> 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="">