app/src/main/java/com/example/firstapp/adapter/ExpressAdapter.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/adapter/FinanceAdapter.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/adapter/FlightAdapter.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/adapter/IncomeAdapter.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/adapter/TrainAdapter.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/database/service/ApiService.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/network/ResponseInterceptor.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/network/TokenExpiredInterceptor.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
app/src/main/java/com/example/firstapp/adapter/ExpressAdapter.kt
@@ -44,9 +44,29 @@ private var currentGroup: ExpressGroup? = null init { // 设置固定高度和禁用嵌套滚动来解决滑动问题 binding.rvPackages.apply { layoutManager = LinearLayoutManager(context) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { // 禁用内部RecyclerView的垂直滚动 return false } // 确保测量所有子项,防止部分内容不可见 override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获可能的异常,防止崩溃 } } } adapter = packagesAdapter // 禁用嵌套滚动,让外部RecyclerView处理所有滚动 isNestedScrollingEnabled = false // 启用回收视图缓存 setItemViewCacheSize(20) setHasFixedSize(true) } } @@ -54,7 +74,13 @@ currentGroup = group binding.tvStationName.text = group.stationName binding.tvPackageCount.text = "共${group.packages.size}个包裹" // 确保所有数据都被更新 packagesAdapter.submitList(null) packagesAdapter.submitList(group.packages) // 请求布局刷新 binding.rvPackages.requestLayout() } fun setOnPackageClickListener(listener: (ExpressGroup, ExpressPackage) -> Unit) { @@ -80,13 +106,21 @@ holder.bind(pack) } // 防止部分内容不显示 override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemExpressPackageHomeBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val pack = getItem(adapterPosition) onPackageClick(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackageClick(pack) } } } @@ -125,13 +159,21 @@ holder.bind(pack) } // 防止部分内容不显示 override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemPackageBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.ivPackageStatus.setOnClickListener { val pack = getItem(adapterPosition) onPackagePickup(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackagePickup(pack) } } binding.root.setOnClickListener(null) app/src/main/java/com/example/firstapp/adapter/FinanceAdapter.kt
@@ -46,9 +46,29 @@ private var currentGroup: FinanceGroup? = null init { // 设置固定高度和禁用嵌套滚动来解决滑动问题 binding.rvPackages.apply { layoutManager = LinearLayoutManager(context) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { // 禁用内部RecyclerView的垂直滚动 return false } // 确保测量所有子项,防止部分内容不可见 override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获可能的异常,防止崩溃 } } } adapter = packagesAdapter // 禁用嵌套滚动,让外部RecyclerView处理所有滚动 isNestedScrollingEnabled = false // 启用回收视图缓存 setItemViewCacheSize(20) setHasFixedSize(true) } } @@ -56,7 +76,13 @@ currentGroup = group binding.tvStationName.text = group.stationName binding.tvPackageCount.text = "共${group.packages.size}笔账单" // 确保所有数据都被更新 packagesAdapter.submitList(null) packagesAdapter.submitList(group.packages) // 请求布局刷新 binding.rvPackages.requestLayout() } fun setOnPackageClickListener(listener: (FinanceGroup, FinancePackage) -> Unit) { @@ -81,14 +107,22 @@ val pack = getItem(position) holder.bind(pack) } // 防止部分内容不显示 override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemFinancePackageHomeBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val pack = getItem(adapterPosition) onPackageClick(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackageClick(pack) } } } @@ -126,14 +160,22 @@ val pack = getItem(position) holder.bind(pack) } // 防止部分内容不显示 override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemFinanceBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.ivPackageStatus.setOnClickListener { val pack = getItem(adapterPosition) onPackagePickup(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackagePickup(pack) } } binding.root.setOnClickListener(null) app/src/main/java/com/example/firstapp/adapter/FlightAdapter.kt
@@ -47,8 +47,23 @@ init { binding.rvPackages.apply { layoutManager = LinearLayoutManager(context) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { return false } override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获可能的异常,防止崩溃 } } } adapter = packagesAdapter isNestedScrollingEnabled = false setItemViewCacheSize(20) setHasFixedSize(true) } } @@ -56,7 +71,11 @@ currentGroup = group binding.tvStationName.text = group.stationName binding.tvPackageCount.text = "共${group.packages.size}张机票" packagesAdapter.submitList(null) packagesAdapter.submitList(group.packages) binding.rvPackages.requestLayout() } fun setOnPackageClickListener(listener: (FlightGroup, FlightPackage) -> Unit) { @@ -82,13 +101,20 @@ holder.bind(pack) } override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemFlightPackageHomeBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val pack = getItem(adapterPosition) onPackageClick(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackageClick(pack) } } } @@ -127,13 +153,20 @@ holder.bind(pack) } override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemFlightBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.ivPackageStatus.setOnClickListener { val pack = getItem(adapterPosition) onPackagePickup(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackagePickup(pack) } } binding.root.setOnClickListener(null) app/src/main/java/com/example/firstapp/adapter/IncomeAdapter.kt
@@ -42,8 +42,23 @@ init { binding.rvPackages.apply { layoutManager = LinearLayoutManager(context) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { return false } override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获可能的异常,防止崩溃 } } } adapter = packagesAdapter isNestedScrollingEnabled = false setItemViewCacheSize(20) setHasFixedSize(true) } } @@ -51,7 +66,11 @@ currentGroup = group binding.tvStationName.text = group.stationName binding.tvPackageCount.text = "共${group.packages.size}笔收入" packagesAdapter.submitList(null) packagesAdapter.submitList(group.packages) binding.rvPackages.requestLayout() } } } @@ -71,13 +90,20 @@ holder.bind(pack) } override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemIncomePackageHomeBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val pack = getItem(adapterPosition) onPackageClick(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackageClick(pack) } } } app/src/main/java/com/example/firstapp/adapter/TrainAdapter.kt
@@ -47,8 +47,23 @@ init { binding.rvPackages.apply { layoutManager = LinearLayoutManager(context) layoutManager = object : LinearLayoutManager(context) { override fun canScrollVertically(): Boolean { return false } override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { try { super.onLayoutChildren(recycler, state) } catch (e: IndexOutOfBoundsException) { // 捕获可能的异常,防止崩溃 } } } adapter = packagesAdapter isNestedScrollingEnabled = false setItemViewCacheSize(20) setHasFixedSize(true) } } @@ -56,7 +71,11 @@ currentGroup = group binding.tvStationName.text = group.stationName binding.tvPackageCount.text = "共${group.packages.size}张车票" packagesAdapter.submitList(null) packagesAdapter.submitList(group.packages) binding.rvPackages.requestLayout() } fun setOnPackageClickListener(listener: (TrainGroup, TrainPackage) -> Unit) { @@ -82,13 +101,20 @@ holder.bind(pack) } override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemTrainPackageHomeBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { val pack = getItem(adapterPosition) onPackageClick(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackageClick(pack) } } } @@ -127,13 +153,20 @@ holder.bind(pack) } override fun getItemCount(): Int { return currentList.size } inner class ViewHolder(private val binding: ItemTrainBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.ivPackageStatus.setOnClickListener { val pack = getItem(adapterPosition) onPackagePickup(pack) val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val pack = getItem(position) onPackagePickup(pack) } } binding.root.setOnClickListener(null) app/src/main/java/com/example/firstapp/database/service/ApiService.kt
@@ -1,6 +1,7 @@ package com.example.firstapp.database.service import TokenResponse import android.content.Context import com.example.firstapp.database.entity.ApiResponse import com.example.firstapp.database.entity.KeywordConfig import com.example.firstapp.database.request.ProductOrdersRequest @@ -16,6 +17,8 @@ import com.example.firstapp.model.CategoryConfig import com.example.firstapp.model.CategoryConfigSync import com.example.firstapp.network.AuthInterceptor import com.example.firstapp.network.ResponseInterceptor import com.example.firstapp.network.TokenExpiredInterceptor import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.RequestBody @@ -72,9 +75,11 @@ @POST("api/account/close") suspend fun closeAccount(): AccountCloseResponse fun getUserCategories(currentUserId: String): List<CategoryConfig> @GET("api/categoryConfig/getByUserId/{userId}") suspend fun getUserCategories(@Path("userId") currentUserId: String): List<CategoryConfig> fun saveUserCategories(categoryConfigSync: CategoryConfigSync) @POST("api/categoryConfig/saveOrUpdate/") suspend fun saveUserCategories(@Body categoryConfigSync: CategoryConfigSync) } @@ -84,25 +89,37 @@ // private const val BASE_URL ="http://192.168.1.213:8080/flower/" private const val BASE_URL ="http://14.103.144.28:8080/flower/" private lateinit var appContext: Context // 初始化方法,需要在Application中调用 fun init(context: Context) { appContext = context.applicationContext } // 创建OkHttpClient,配置拦截器和超时时间 private val okHttpClient = OkHttpClient.Builder() .addInterceptor(AuthInterceptor()) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() private val okHttpClient by lazy { OkHttpClient.Builder() .addInterceptor(AuthInterceptor()) .addInterceptor(TokenExpiredInterceptor(appContext)) .addInterceptor(ResponseInterceptor(appContext)) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build() } //添加Gson解析器,用于自动将JSON响应转换为Kotlin/Java对象 private val retrofit = Retrofit .Builder() .client(okHttpClient) .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() private val retrofit by lazy { Retrofit .Builder() .client(okHttpClient) .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() } //通过动态代理技术创建ApiService接口的具体实现类 val apiService:ApiService = retrofit.create(ApiService::class.java) val apiService by lazy { retrofit.create(ApiService::class.java) } } app/src/main/java/com/example/firstapp/network/ResponseInterceptor.kt
对比新文件 @@ -0,0 +1,85 @@ package com.example.firstapp.network import android.content.Context import android.content.Intent import android.widget.Toast import com.example.firstapp.activity.LoginActivity import com.example.firstapp.utils.PreferencesManager import com.google.gson.JsonParser import okhttp3.Interceptor import okhttp3.Response import okhttp3.ResponseBody import okio.Buffer import kotlin.concurrent.thread /** * 响应拦截器 - 处理业务错误码 * 根据后端提供的接口规范,检测token失效的情况 */ class ResponseInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) try { // 只处理成功的响应 if (response.isSuccessful) { val responseBody = response.body if (responseBody != null) { val source = responseBody.source() source.request(Long.MAX_VALUE) val buffer = source.buffer.clone() val responseBodyString = buffer.readUtf8() try { // 解析JSON val jsonObject = JsonParser.parseString(responseBodyString).asJsonObject // 检查业务状态码 if (jsonObject.has("code")) { val code = jsonObject.get("code").asString // 如果状态码表示token失效或登录过期 if (code == "401" || code == "-1" || code == "1001") { // 检查返回的错误消息 val message = if (jsonObject.has("msg")) jsonObject.get("msg").asString else "登录已失效,请重新登录" if (message.contains("token") || message.contains("登录") || message.contains("认证") || message.contains("授权")) { // 清除本地token PreferencesManager.clearUserData() // 在主线程中显示提示并跳转到登录页面 thread { android.os.Handler(context.mainLooper).post { Toast.makeText(context, message, Toast.LENGTH_LONG).show() // 创建跳转到登录页面的Intent,并添加清除任务栈的标志 val intent = Intent(context, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } context.startActivity(intent) } } } } } } catch (e: Exception) { e.printStackTrace() } // 因为我们已经读取了响应体,需要重新创建一个新的响应体 val newResponseBody = ResponseBody.create(responseBody.contentType(), responseBodyString) return response.newBuilder().body(newResponseBody).build() } } } catch (e: Exception) { e.printStackTrace() } return response } } app/src/main/java/com/example/firstapp/network/TokenExpiredInterceptor.kt
@@ -1,19 +1,54 @@ package com.example.firstapp.network import android.content.Context import android.content.Intent import android.widget.Toast import com.example.firstapp.activity.LoginActivity import com.example.firstapp.utils.PreferencesManager import okhttp3.Interceptor import okhttp3.Response import kotlin.concurrent.thread class TokenExpiredInterceptor : Interceptor { class TokenExpiredInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) // 如果返回401,说明token可能过期 if (response.code == 401) { PreferencesManager.clearUserData() // 清除本地token // TODO: 处理token过期,例如跳转到登录页面 // 如果返回401或后端自定义的token失效状态码,说明token可能过期 if (response.code == 401 || isTokenInvalid(response)) { // 清除本地token PreferencesManager.clearUserData() // 在主线程中显示提示并跳转到登录页面 thread { android.os.Handler(context.mainLooper).post { Toast.makeText(context, "登录已失效,请重新登录", Toast.LENGTH_LONG).show() // 创建跳转到登录页面的Intent,并添加清除任务栈的标志 val intent = Intent(context, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } context.startActivity(intent) } } } return response } // 检查响应是否表示token失效 private fun isTokenInvalid(response: Response): Boolean { try { // 尝试读取响应体,检查自定义的错误码 // 注意:这会消耗响应体,如果需要在后续处理中使用响应体,需要克隆 val responseBody = response.peekBody(4096).string() // 根据您的后端逻辑,检查是否包含token失效的提示 // 这里假设后端在返回JSON中包含了错误码和消息 return responseBody.contains("\"code\":\"401\"") || responseBody.contains("token失效") || responseBody.contains("请重新登录") } catch (e: Exception) { return false } } } app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
@@ -50,8 +50,9 @@ val unreadReminderCount: LiveData<Int> = _unreadReminderCount private lateinit var secureStorage: SecureStorage private lateinit var currentUserId: String private var currentUserId: String = "" private lateinit var reminderRecordRepository: ReminderRecordRepository private var categoriesLoaded = false init { // 初始化时加载包裹列表数据 @@ -64,7 +65,13 @@ secureStorage = SecureStorage(context) currentUserId = userId reminderRecordRepository = ReminderRecordRepository(context) loadCategories() // 只在首次加载或者用户修改分类后加载分类数据 if (!categoriesLoaded) { loadCategories() categoriesLoaded = true } // 初始化时更新可见分类 _categories.value?.let { updateVisibleCategories(it) } // 加载未读提醒数量 @@ -188,9 +195,6 @@ private fun loadCategories() { viewModelScope.launch { try { // 先尝试从本地获取配置 val localCategories = secureStorage.getCategories(currentUserId) // 默认完整分类列表 val fullCategories = listOf( CategoryConfig(1, "快递", 0, true), @@ -206,30 +210,73 @@ CategoryConfig(2, "还款", 1, true) ) if (localCategories.isNotEmpty()) { // 如果本地有配置,直接使用本地配置 _categories.value = localCategories } else { try { // 尝试从服务器获取用户信息判断是否是会员 val savedPhone = PreferencesManager.getPhone() val response = RetrofitClient.apiService.getUserInfo(savedPhone ?: "") val isMember = response.code == "0" && response.data?.isMember == true try { // 获取会员状态 val savedPhone = PreferencesManager.getPhone() val userResponse = RetrofitClient.apiService.getUserInfo(savedPhone ?: "") val isMember = userResponse.code == "0" && userResponse.data?.isMember == true // 从用户信息中获取正确的userId if (userResponse.code == "0" && userResponse.data != null) { currentUserId = userResponse.data?.id.toString() } // 根据会员状态设置默认分类 val defaultCategories = if (isMember) fullCategories else basicCategories _categories.value = defaultCategories secureStorage.saveCategories(currentUserId, defaultCategories) // 同步到服务器 // 首先检查本地是否有缓存的分类配置 val localCategories = secureStorage.getCategories(currentUserId) if (localCategories.isNotEmpty()) { // 使用本地缓存的配置 _categories.value = localCategories } else { // 本地无缓存,尝试从服务器获取 try { syncCategoriesToServer(defaultCategories) val serverCategories = RetrofitClient.apiService.getUserCategories(currentUserId) if (serverCategories.isNotEmpty()) { // 服务器有配置,使用服务器配置 // 如果不是会员,需要过滤掉会员专属分类 val filteredCategories = if (isMember) { serverCategories } else { serverCategories.filter { it.name == "快递" || it.name == "还款" } } _categories.value = filteredCategories // 同时更新本地缓存 secureStorage.saveCategories(currentUserId, filteredCategories) // 同步回服务器(如果有变化) if (filteredCategories.size != serverCategories.size) { syncCategoriesToServer(filteredCategories) } } else { // 服务器返回空,根据会员状态设置默认分类 val defaultCategories = if (isMember) fullCategories else basicCategories _categories.value = defaultCategories // 更新本地缓存 secureStorage.saveCategories(currentUserId, defaultCategories) // 同步到服务器 syncCategoriesToServer(defaultCategories) } } catch (e: Exception) { Log.e("HomeViewModel", "Failed to sync categories: ${e.message}") // 服务器获取失败,使用默认分类 Log.e("HomeViewModel", "Failed to get categories from server: ${e.message}") val defaultCategories = if (isMember) fullCategories else basicCategories _categories.value = defaultCategories secureStorage.saveCategories(currentUserId, defaultCategories) } } catch (e: Exception) { // 如果获取用户信息失败,使用基础分类 } } catch (e: Exception) { // 网络连接失败,尝试从本地获取配置 Log.e("HomeViewModel", "Failed to get user info: ${e.message}") val localCategories = secureStorage.getCategories(currentUserId) if (localCategories.isNotEmpty()) { // 使用本地缓存的配置 _categories.value = localCategories } else { // 本地也没有配置,使用基础分类 _categories.value = basicCategories // 更新本地缓存 secureStorage.saveCategories(currentUserId, basicCategories) } } @@ -239,20 +286,26 @@ } catch (e: Exception) { Log.e("HomeViewModel", "Failed to load categories: ${e.message}") // 出现异常时,使用基础分类 val basicCategories = listOf( CategoryConfig(1, "快递", 0, true), CategoryConfig(2, "还款", 1, true) ) _categories.value = basicCategories // 更新可见分类 updateVisibleCategories(basicCategories) } } } 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}") } private suspend fun syncCategoriesToServer(categories: List<CategoryConfig>) { try { RetrofitClient.apiService.saveUserCategories( CategoryConfigSync(currentUserId, categories) ) } catch (e: Exception) { // 同步失败,可以稍后重试或者显示提示 Log.e("CategorySync", "Failed to sync categories: ${e.message}") } } @@ -261,9 +314,17 @@ // 保存到本地存储 secureStorage.saveCategories(currentUserId, categories) // 同步到服务器 syncCategoriesToServer(categories) viewModelScope.launch { try { syncCategoriesToServer(categories) } catch (e: Exception) { Log.e("HomeViewModel", "Failed to sync categories: ${e.message}") } } // 更新可见分类 updateVisibleCategories(categories) // 标记分类已被修改 categoriesLoaded = true } private fun updateVisibleCategories(categories: List<CategoryConfig>) { @@ -287,10 +348,17 @@ } } // 添加方法以强制刷新分类 fun refreshCategories() { categoriesLoaded = false loadCategories() } // 登出时不再清除本地数据 fun logout() { // 只清除内存中的数据 _categories.value = emptyList() categoriesLoaded = false } }