diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f23bd51..9488e5b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,6 +115,11 @@ android:theme="@style/Theme.DishMatch.NoSplash" android:screenOrientation="portrait" tools:ignore="DiscouragedApi,LockedOrientationActivity" /> + ) : + BaseQuickAdapter(list) { + + /** ViewHolder,持有列表项 ViewBinding */ + inner class VH(val binding: ListItemCollectedFoodBinding) : QuickViewHolder(binding.root) + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val binding = ListItemCollectedFoodBinding.inflate(LayoutInflater.from(context), parent, false) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodItem?) { + item ?: return + holder.binding.tvFoodName.text = item.foodName.split("WP").first() + holder.binding.tvCollectCount.text = "已采集${item.collectCount}" + } +} diff --git a/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt b/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt index 633a665..3baccbb 100644 --- a/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt @@ -40,8 +40,6 @@ open class BaseActivity : AppCompatActivity() { private lateinit var binding: ActivityBaseBinding - - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 提前隐藏系统栏,避免窗口创建时导航栏闪烁 diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt index 68f87f4..0540024 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt @@ -2,7 +2,9 @@ package com.shuwei.dish.match.db import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.shuwei.dish.match.entity.CollectedFoodItem import com.shuwei.dish.match.entity.CookFoodEntity +import com.shuwei.dish.match.objbox.ObjectBox import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.entity.SeasoningSlotEntity @@ -248,4 +250,39 @@ class DbViewModel : ViewModel() { } //-------------------------------------------------------------------------------------------------------- + + /** + * 已采集食材列表 UI 状态流 + * 元素为按名称分组后的(名称,数量)数据 + */ + private val _collectedFoodListState = MutableStateFlow>(emptyList()) + val collectedFoodListState: StateFlow> = _collectedFoodListState.asStateFlow() + + /** + * 加载已采集食材列表,按名称分组统计数量 + * 使用属性查询只读 name 字段,不加载 foodVector,节省内存 + * @param nameFilter 名称过滤关键词,为空时加载全部 + */ + fun loadCollectedFoodList(nameFilter: String? = null) { + viewModelScope.launch { + val result = ObjectBox.queryFoodNameCounts(nameFilter) + _collectedFoodListState.value = result.map { (name, count) -> + CollectedFoodItem(foodName = name, collectCount = count) + } + } + } + + /** + * 删除指定名称的所有采集记录,删除完成后刷新列表 + * @param foodName 食材名称 + * @param nameFilter 当前搜索关键词,用于删除后刷新列表 + */ + fun removeCollectedFood(foodName: String, nameFilter: String? = null) { + viewModelScope.launch { + ObjectBox.remove(foodName) + loadCollectedFoodList(nameFilter) + } + } + + //-------------------------------------------------------------------------------------------------------- } \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/CommonDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/CommonDialog.kt index 25d42f9..da271f0 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/CommonDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/CommonDialog.kt @@ -23,7 +23,7 @@ open class CommonDialog( private lateinit var binding: DialogCommonBinding private var titleText: String? = null - private var contentText: String? = null + private var contentText: CharSequence? = null private var negativeText = "取消" private var positiveText = "确认" private var neutralText: String? = null @@ -36,7 +36,7 @@ open class CommonDialog( fun setTitle(text: String): CommonDialog = apply { titleText = text } /** 设置内容,为空时隐藏 */ - fun setContent(text: String): CommonDialog = apply { contentText = text } + fun setContent(text: CharSequence): CommonDialog = apply { contentText = text } /** 设置左侧取消按钮文字及点击回调(3按钮模式下对应顶部按钮) */ fun setNegativeButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply { diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt index f42b31f..e70ee05 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/FoodSearchDialog.kt @@ -101,7 +101,7 @@ class FoodSearchDialog( } binding.ivSearch.setOnClickListener { searchGoods(it) } - binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it.context, it) } + binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) } initObserver() @@ -142,7 +142,7 @@ class FoodSearchDialog( } pageNo = 1 getGoodsList() - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } /** diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt index eb2d76c..fdc97ce 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSearchDialog.kt @@ -39,7 +39,6 @@ import com.shuwei.dish.match.utils.ext.buildSpannableString import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.roundedDecimalPlace import com.shuwei.dish.match.utils.ext.toast -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** @@ -133,7 +132,7 @@ class SeasoningSearchDialog( } binding.ivSearch.setOnClickListener { searchGoods(it) } - binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it.context, it) } + binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) } // 去皮按钮 binding.tvClear.setOnClickListener { @@ -193,7 +192,7 @@ class SeasoningSearchDialog( } pageNo = 1 getGoodsList() - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } /** diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt index 1ebe7bb..a55443f 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/SeasoningSelectDialog.kt @@ -106,7 +106,7 @@ class SeasoningSelectDialog( } binding.ivSearch.setOnClickListener { searchGoods(it) } - binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it.context, it) } + binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) } } /** 弹窗显示时自动加载默认调料列表 */ @@ -126,7 +126,7 @@ class SeasoningSelectDialog( } pageNo = 1 getGoodsList() - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard( v) } /** diff --git a/app/src/main/java/com/shuwei/dish/match/entity/CollectedFoodItem.kt b/app/src/main/java/com/shuwei/dish/match/entity/CollectedFoodItem.kt new file mode 100644 index 0000000..0e4e296 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/entity/CollectedFoodItem.kt @@ -0,0 +1,11 @@ +package com.shuwei.dish.match.entity + +/** + * 已采集食材列表项数据模型 + * @param foodName 食材名称 + * @param collectCount 已采集数量 + */ +data class CollectedFoodItem( + val foodName: String = "", + val collectCount: Int = 0 +) diff --git a/app/src/main/java/com/shuwei/dish/match/objbox/ObjectBox.kt b/app/src/main/java/com/shuwei/dish/match/objbox/ObjectBox.kt index 34110ac..c5a0cd6 100644 --- a/app/src/main/java/com/shuwei/dish/match/objbox/ObjectBox.kt +++ b/app/src/main/java/com/shuwei/dish/match/objbox/ObjectBox.kt @@ -213,6 +213,31 @@ object ObjectBox { getBox()?.query(Food_.name.equal(name))?.build()?.remove() } + /** + * 按名称分组统计采集数量 + * 使用属性查询只加载 name 字段,不加载 foodVector,节省大量内存 + * @param nameFilter 名称过滤关键词,为空时查询全部 + * @return 按名称分组后的 (名称, 数量) 列表,按名称排序 + */ + suspend fun queryFoodNameCounts(nameFilter: String? = null) = safeDbOp { + val box = getBox() ?: return@safeDbOp emptyList>() + val query = if (!nameFilter.isNullOrBlank()) { + box.query(Food_.name.contains(nameFilter)).build() + } else { + box.query().build() + } + try { + // 只查 name 字段,完全不加载 foodVector + query.property(Food_.name).findStrings() + .groupingBy { it } + .eachCount() + .map { (name, count) -> Pair(name, count) } + .sortedBy { it.first } + } finally { + query.close() + } + } ?: emptyList() + private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性 private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录 private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录 diff --git a/app/src/main/java/com/shuwei/dish/match/ui/CollectedFoodActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/CollectedFoodActivity.kt new file mode 100644 index 0000000..18fb7cc --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/ui/CollectedFoodActivity.kt @@ -0,0 +1,192 @@ +package com.shuwei.dish.match.ui + +import android.annotation.SuppressLint +import android.graphics.Color +import android.graphics.Typeface +import android.os.Bundle +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.shuwei.dish.match.R +import com.shuwei.dish.match.adapter.CollectedFoodAdapter +import com.shuwei.dish.match.base.BaseActivity +import com.shuwei.dish.match.databinding.ActivityCollectedFoodBinding +import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding +import com.shuwei.dish.match.dialog.CommonDialog +import com.shuwei.dish.match.entity.CollectedFoodItem +import com.shuwei.dish.match.utils.KeyboardUtil +import com.shuwei.dish.match.utils.ext.appendText +import com.shuwei.dish.match.utils.ext.buildSpannableString +import com.shuwei.dish.match.utils.ext.gone +import com.shuwei.dish.match.utils.ext.toast +import com.shuwei.dish.match.utils.ext.visible +import kotlinx.coroutines.launch + +/** + * 已采集食材列表页面 + * 展示所有已采集的食材及其采集数量,支持搜索和删除 + */ +class CollectedFoodActivity : BaseActivity() { + + private lateinit var binding: ActivityCollectedFoodBinding + + private val foodList = mutableListOf() + + private var emptyViewBinding: LayoutEmptyViewBinding? = null + + private val adapter by lazy { + CollectedFoodAdapter(foodList).apply { + isStateViewEnable = true + // 点击删除按钮,移除对应食材 + addOnItemChildClickListener(R.id.ivDelete) { _, _, position -> + showDeleteDialog(foodList[position].foodName) + } + setOnItemClickListener { _, view, _ -> + KeyboardUtil.hideKeyboard(view) + } + } + } + + /** + * 删除弹窗 + */ + private fun showDeleteDialog(foodName: String) { + val showFoodName = foodName.split("WP").first() + val separator = if (showFoodName.length <= 10) "\n" else "" + val content = buildSpannableString { + appendText("食材:") + appendText(showFoodName, StyleSpan(Typeface.BOLD), ForegroundColorSpan(Color.BLACK)) + appendText(" 删除后无法恢复,${separator}确认要删除吗?") + } + CommonDialog(this) + .setTitle("温馨提示") + .setContent(content) + .setNegativeButton("取消") + .setPositiveButton("确认") { + appViewModel.removeCollectedFood( + foodName = foodName, + nameFilter = binding.editSearch.text.toString() + ) + } + .show() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + statusBarDarkFont(enable = true) + binding = ActivityCollectedFoodBinding.inflate(layoutInflater) + setContentView(binding.root) + setHeaderBackground() + binding.root.setOnClickListener { v -> + KeyboardUtil.hideKeyboard(v) + } + setTitleBar(titleBarAction = { + it.visible() + it.setOnClickListener { v -> + KeyboardUtil.hideKeyboard(v) + } + }, titleAction = { + it.text = "已采集食材" + }, rightIconActon = { + it.gone() + }, backAction = { + it.visible() + it.setOnClickListener { + finish() + } + }) + initRecyclerView() + initSearch() + initObserver() + loadCollectedFoodList() + } + + private fun loadCollectedFoodList(name: String = "") { + appViewModel.loadCollectedFoodList(nameFilter = name) + } + + /** + * 收集 searchFoodState,统一处理 Loading / Success / Error 状态 + */ + @SuppressLint("NotifyDataSetChanged") + private fun initObserver() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + appViewModel.collectedFoodListState.collect { items -> + foodList.clear() + foodList.addAll(items) + adapter.notifyDataSetChanged() + if (items.isEmpty()) { + loadEmptyView() + } + } + } + } + } + + /** 展示空视图 */ + private fun loadEmptyView() { + if (emptyViewBinding == null) { + emptyViewBinding = LayoutEmptyViewBinding.inflate( + LayoutInflater.from(this), binding.rvCollectedFood, false + ) + } + emptyViewBinding!!.tvContent.text = "暂无数据" + emptyViewBinding!!.tvSubContent.text = "未查询到食材信息" + adapter.stateView = emptyViewBinding!!.root + } + + /** 初始化列表 */ + private fun initRecyclerView() { + binding.rvCollectedFood.run { + layoutManager = LinearLayoutManager(this@CollectedFoodActivity) + adapter = this@CollectedFoodActivity.adapter + } + } + + /** 初始化搜索栏交互 */ + private fun initSearch() { + binding.btnSearch.setOnClickListener { v -> + performSearch { + toast(binding.editSearch.hint.toString()) + } + KeyboardUtil.hideKeyboard(v) + } + binding.editSearch.run { + addTextChangedListener { + performSearch { + loadCollectedFoodList() + } + } + setOnEditorActionListener { v, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + performSearch { + toast(binding.editSearch.hint.toString()) + } + KeyboardUtil.hideKeyboard(v) + true + } else { + false + } + } + } + } + + /** 根据关键词过滤食材列表 */ + @SuppressLint("NotifyDataSetChanged") + private fun performSearch(action: () -> Unit) { + val keyword = binding.editSearch.text.toString().trim() + if (keyword.isEmpty()) { + action() + return + } + // 根据关键词从数据库查询并刷新列表 + loadCollectedFoodList(keyword) + } +} diff --git a/app/src/main/java/com/shuwei/dish/match/ui/DishSamplingActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/DishSamplingActivity.kt index f090f03..78b77ff 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/DishSamplingActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/DishSamplingActivity.kt @@ -256,7 +256,7 @@ class DishSamplingActivity : BaseActivity() { setSelection(text.length) } } - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } private fun searchDishType() { diff --git a/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt index 30a69c3..2e34410 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/FoodSearchActivity.kt @@ -119,7 +119,7 @@ class FoodSearchActivity : BaseActivity() { binding.ivDishSearch.setOnClickListener { v -> pageNo = 1 getInputAndSearch() - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } binding.refreshLayout.setEnableRefresh(true) binding.refreshLayout.setEnableLoadMore(false) @@ -136,13 +136,13 @@ class FoodSearchActivity : BaseActivity() { queryListInfo(foodName!!) } binding.root.setOnClickListener { v -> - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } binding.etInputDish.let { v -> v.addOnActionSearchListener { pageNo = 1 getInputAndSearch() - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard( v) } } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt index c1954fd..08f67c7 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/PrepareCookActivity.kt @@ -551,7 +551,7 @@ class PrepareCookActivity : BaseActivity() { setSelection(text.length) } } - KeyboardUtil.hideKeyboard(v.context, v) + KeyboardUtil.hideKeyboard(v) } } \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SelectDishActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SelectDishActivity.kt index ea25d60..f3ac5b6 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SelectDishActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SelectDishActivity.kt @@ -120,7 +120,7 @@ class SelectDishActivity : BaseActivity() { putExtra(FoodSearchActivity.DINNER_TYPE, dinnerType) putExtra(FoodSearchActivity.FOOD_NAME, searchText) } - KeyboardUtil.hideKeyboard(this, binding.etInputDish) + KeyboardUtil.hideKeyboard(binding.etInputDish) } private fun loadFragment() { diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt index 2e70f9a..92350f4 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/CollectFragment.kt @@ -1,6 +1,7 @@ package com.shuwei.dish.match.ui.fragment import android.annotation.SuppressLint +import android.content.Intent import android.content.Context import android.graphics.Bitmap import android.net.Uri @@ -22,6 +23,7 @@ import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.dialog.Loading import com.shuwei.dish.match.objbox.FoodCollectionBean import com.shuwei.dish.match.objbox.FoodModule +import com.shuwei.dish.match.ui.CollectedFoodActivity import com.shuwei.dish.match.ui.SingleFragmentActivity import com.shuwei.dish.match.utils.BitmapSaver import com.shuwei.dish.match.utils.CameraUtils @@ -243,7 +245,7 @@ class CollectFragment : BaseFragment(){ } binding.btnCollectedFood.setOnClickListener { -// startActivity(Intent(requireActivity(), CollectedFoodActivity::class.java)) + startActivity(Intent(requireActivity(), CollectedFoodActivity::class.java)) } binding.btnTakePhoto.clickWithDebounce { val count = foodCollectionList.count { it.bitmap != null } diff --git a/app/src/main/java/com/shuwei/dish/match/utils/KeyboardUtil.kt b/app/src/main/java/com/shuwei/dish/match/utils/KeyboardUtil.kt index e978de1..a879997 100644 --- a/app/src/main/java/com/shuwei/dish/match/utils/KeyboardUtil.kt +++ b/app/src/main/java/com/shuwei/dish/match/utils/KeyboardUtil.kt @@ -7,7 +7,8 @@ import android.view.inputmethod.InputMethodManager object KeyboardUtil { - fun hideKeyboard(context: Context, view: View) { + fun hideKeyboard(view: View) { + val context = view.context val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(view.windowToken, 0) view.clearFocus() // 清除焦点避免键盘再次弹出 diff --git a/app/src/main/res/drawable/ic_close_red.xml b/app/src/main/res/drawable/ic_close_red.xml new file mode 100644 index 0000000..04803cb --- /dev/null +++ b/app/src/main/res/drawable/ic_close_red.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_collected_food.xml b/app/src/main/res/layout/activity_collected_food.xml new file mode 100644 index 0000000..3368d0d --- /dev/null +++ b/app/src/main/res/layout/activity_collected_food.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_collect.xml b/app/src/main/res/layout/fragment_collect.xml index 2051362..57b0808 100644 --- a/app/src/main/res/layout/fragment_collect.xml +++ b/app/src/main/res/layout/fragment_collect.xml @@ -20,7 +20,7 @@ android:layout_marginStart="32dp" android:layout_marginTop="30dp" android:drawablePadding="17dp" - android:text="餐品采集" + android:text="食材采集" android:textColor="@color/black" android:textSize="36sp" android:textStyle="bold" @@ -35,7 +35,7 @@ android:layout_marginEnd="32dp" android:gravity="center_vertical" android:includeFontPadding="false" - android:text="已采集餐品" + android:text="已采集食材" android:textColor="#ff5e7585" android:textSize="30sp" android:textStyle="bold" diff --git a/app/src/main/res/layout/list_item_collected_food.xml b/app/src/main/res/layout/list_item_collected_food.xml new file mode 100644 index 0000000..00e0ba1 --- /dev/null +++ b/app/src/main/res/layout/list_item_collected_food.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + +