feat(activity): 新增已采集食材列表页,支持搜索、删除及空视图展示

This commit is contained in:
2026-04-23 16:51:57 +08:00
parent 9f0671410f
commit 58fb78c7bd
21 changed files with 455 additions and 21 deletions
+5
View File
@@ -115,6 +115,11 @@
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.CollectedFoodActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.SlaveActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
@@ -0,0 +1,31 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemCollectedFoodBinding
import com.shuwei.dish.match.entity.CollectedFoodItem
/**
* 已采集食材列表适配器
* 展示食材名称和已采集数量,支持删除操作
*/
class CollectedFoodAdapter(list: MutableList<CollectedFoodItem>) :
BaseQuickAdapter<CollectedFoodItem, CollectedFoodAdapter.VH>(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}"
}
}
@@ -40,8 +40,6 @@ open class BaseActivity : AppCompatActivity() {
private lateinit var binding: ActivityBaseBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 提前隐藏系统栏,避免窗口创建时导航栏闪烁
@@ -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<List<CollectedFoodItem>>(emptyList())
val collectedFoodListState: StateFlow<List<CollectedFoodItem>> = _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)
}
}
//--------------------------------------------------------------------------------------------------------
}
@@ -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 {
@@ -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)
}
/**
@@ -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)
}
/**
@@ -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)
}
/**
@@ -0,0 +1,11 @@
package com.shuwei.dish.match.entity
/**
* 已采集食材列表项数据模型
* @param foodName 食材名称
* @param collectCount 已采集数量
*/
data class CollectedFoodItem(
val foodName: String = "",
val collectCount: Int = 0
)
@@ -213,6 +213,31 @@ object ObjectBox {
getBox<Food>()?.query(Food_.name.equal(name))?.build()?.remove()
}
/**
* 按名称分组统计采集数量
* 使用属性查询只加载 name 字段,不加载 foodVector,节省大量内存
* @param nameFilter 名称过滤关键词,为空时查询全部
* @return 按名称分组后的 (名称, 数量) 列表,按名称排序
*/
suspend fun queryFoodNameCounts(nameFilter: String? = null) = safeDbOp {
val box = getBox<Food>() ?: return@safeDbOp emptyList<Pair<String, Int>>()
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" // 备份目录
@@ -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<CollectedFoodItem>()
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)
}
}
@@ -256,7 +256,7 @@ class DishSamplingActivity : BaseActivity() {
setSelection(text.length)
}
}
KeyboardUtil.hideKeyboard(v.context, v)
KeyboardUtil.hideKeyboard(v)
}
private fun searchDishType() {
@@ -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)
}
}
}
@@ -551,7 +551,7 @@ class PrepareCookActivity : BaseActivity() {
setSelection(text.length)
}
}
KeyboardUtil.hideKeyboard(v.context, v)
KeyboardUtil.hideKeyboard(v)
}
}
@@ -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() {
@@ -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<FragmentCollectBinding>(){
}
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 }
@@ -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() // 清除焦点避免键盘再次弹出
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 红色圆形背景 + 白色 X 图标,用于列表项删除按钮 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- 红色圆形背景 #FFEE4444 -->
<path
android:fillColor="#00BC71"
android:pathData="M24,2C11.85,2 2,11.85 2,24C2,36.15 11.85,46 24,46C36.15,46 46,36.15 46,24C46,11.85 36.15,2 24,2Z" />
<!-- 白色 X 图标 -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M33,16.41L31.59,15L24,22.59L16.41,15L15,16.41L22.59,24L15,31.59L16.41,33L24,25.41L31.59,33L33,31.59L25.41,24L33,16.41Z" />
</vector>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/white"
android:orientation="vertical">
<!-- 搜索栏 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/editSearch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/setting_border_gray3"
android:gravity="center_vertical"
android:hint="输入食材名称"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="47dp"
android:paddingEnd="80dp"
android:textColor="@color/black"
android:textColorHint="#FFC4CFDA"
android:textSize="36sp" />
<ImageView
android:id="@+id/btnSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="15dp"
android:padding="10dp"
android:src="@drawable/ic_search2"
tools:ignore="ContentDescription" />
</FrameLayout>
<!-- 已采集食材列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCollectedFood"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never"
tools:itemCount="5"
tools:listitem="@layout/list_item_collected_food" />
</LinearLayout>
+2 -2
View File
@@ -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"
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="32dp"
android:paddingVertical="24dp">
<!-- 食材名称和采集数量 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="32sp"
android:textStyle="bold"
tools:text="三河小炒" />
<TextView
android:id="@+id/tvCollectCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="#ff5e7585"
android:textSize="26sp"
tools:text="已采集153" />
</LinearLayout>
<!-- 删除按钮 -->
<ImageView
android:id="@+id/ivDelete"
android:layout_width="100dp"
android:layout_height="100dp"
android:padding="25dp"
android:src="@drawable/ic_close_red"
tools:ignore="ContentDescription" />
</LinearLayout>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="32dp"
android:background="#FFE8EDF2" />
</LinearLayout>