feat(fragment): 新增净材种类选择功能及相关数据结构支持

- 新增 rawMaterialsType 字段至 GoodsItem、CookFoodGoodsEntity、SeasoningEntity,DB 升级至 v12
- VectorCollectionFragment 重构食材搜索,点击食材弹出净材种类单选弹窗
- CommonDialog 新增 addContentView 和可拦截确认按钮(setPositiveButtonInterceptable)能力
- Food3Adapter 和 list_item_food3.xml 支持显示已选净材种类副标签
This commit is contained in:
2026-05-09 18:10:07 +08:00
parent a9663a21f0
commit ef13aa8307
11 changed files with 268 additions and 53 deletions
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
@@ -24,6 +25,15 @@ class Food3Adapter(list: MutableList<GoodsItem>) :
text = item.goodsName
isChecked = item.isClicked
}
// 显示当前选中的净材种类,无数据时隐藏
holder.binding.tvRawMaterialsType.run {
if (item.rawMaterialsType.isNullOrEmpty()) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
text = item.rawMaterialsType
}
}
}
override fun onCreateViewHolder(
@@ -25,7 +25,7 @@ import com.shuwei.dish.match.entity.SeasoningSlotEntity
CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
],
version = 11,
version = 12,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@@ -240,6 +240,14 @@ val MIGRATION_10_11 = MigrationImpl(10, 11) { db ->
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN goodsCode TEXT DEFAULT ''")
}
/**
* 版本11→12:为 dm_cook_food_goods 和 dm_seasoning 表新增 rawMaterialsType(净材种类)字段
*/
val MIGRATION_11_12 = MigrationImpl(11, 12) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
}
class DatabaseProvider(private val context: Context) {
// 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy {
@@ -262,6 +270,7 @@ class DatabaseProvider(private val context: Context) {
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.build()
}
}
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
@@ -26,9 +27,14 @@ open class CommonDialog(
private var positiveText = "确认"
private var neutralText: String? = null
private var negativeClick: (() -> Unit)? = null
private var positiveClick: (() -> Unit)? = null
// 返回 true 时关闭弹窗,返回 false 时保持弹窗显示(用于校验不通过的场景)
private var positiveClick: (() -> Boolean)? = null
private var neutralClick: (() -> Unit)? = null
private var dismissCallback: (() -> Unit)? = null
// 待添加到 llContent 的自定义 View,在 onCreate 后挂载
private var pendingContentView: View? = null
// pendingContentView 非空时对 tvContent 的灵活控制回调(可调整可见性、间距等)
private var tvContentConfigurator: ((TextView) -> Unit)? = null
/** 设置标题,为空时隐藏 */
fun setTitle(text: String): CommonDialog = apply { titleText = text }
@@ -42,8 +48,17 @@ open class CommonDialog(
negativeClick = onClick
}
/** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮) */
/** 设置右侧确认按钮文字及点击回调(3按钮模式下对应底部按钮),点击后自动关闭弹窗 */
fun setPositiveButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
positiveText = text
positiveClick = onClick?.let { { it(); true } }
}
/**
* 设置右侧确认按钮文字及可拦截的点击回调
* 回调返回 true 时关闭弹窗,返回 false 时保持弹窗显示(用于校验不通过的场景)
*/
fun setPositiveButtonInterceptable(text: String, onClick: () -> Boolean): CommonDialog = apply {
positiveText = text
positiveClick = onClick
}
@@ -59,6 +74,16 @@ open class CommonDialog(
dismissCallback = callback
}
/**
* 向弹窗自定义内容区域添加 View,调用后 llContent 自动显示
* @param view 要嵌入的自定义布局(如单选列表)
* @param tvContentConfig 可选回调,用于在 pendingContentView 非空时灵活控制 tvContent 的可见性及间距等属性
*/
fun addContentView(view: View, tvContentConfig: ((TextView) -> Unit)? = null): CommonDialog = apply {
pendingContentView = view
tvContentConfigurator = tvContentConfig
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
@@ -88,8 +113,8 @@ open class CommonDialog(
dismiss()
}
binding.btnRight.setOnClickListener {
positiveClick?.invoke()
dismiss()
// 回调返回 false 时阻止关闭,null 或 true 时正常关闭
if (positiveClick?.invoke() != false) dismiss()
}
// 3按钮点击事件(negativeClick/positiveClick 与2按钮模式共用)
@@ -102,11 +127,17 @@ open class CommonDialog(
dismiss()
}
binding.btnBottom.setOnClickListener {
positiveClick?.invoke()
dismiss()
if (positiveClick?.invoke() != false) dismiss()
}
setOnDismissListener { dismissCallback?.invoke() }
// 挂载自定义内容 View,并执行 tvContent 的灵活控制回调
pendingContentView?.let {
binding.llContent.addView(it)
binding.llContent.visibility = View.VISIBLE
tvContentConfigurator?.invoke(binding.tvContent)
}
}
override fun show() {
@@ -69,6 +69,9 @@ class CookFoodGoodsEntity(
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity, Serializable {
@@ -88,6 +91,7 @@ class CookFoodGoodsEntity(
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@@ -31,7 +31,11 @@ data class GoodsItem(
// true-原始数据,false-非原始数据(识图新增)
var isOriginalData: Boolean = true,
// 识图匹配分数,-1 表示不显示
var foodScore: Int = -1
var foodScore: Int = -1,
// 净材种类(当前选中值)
var rawMaterialsType: String? = null,
// 净材种类候选列表(临时字段,逗号分隔,不持久化)
var rawMaterialsTypes: String? = null
) : Serializable {
/**
@@ -53,7 +57,8 @@ data class GoodsItem(
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
/**
@@ -69,6 +74,7 @@ data class GoodsItem(
materialType = materialType,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@@ -61,6 +61,9 @@ data class SeasoningEntity(
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var sort: Int = 9999,
/**
* isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据
@@ -11,6 +11,9 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.view.Gravity
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.camera.view.PreviewView
import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
@@ -25,6 +28,7 @@ import com.shuwei.dish.match.adapter.Food3Adapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentVectorCollectionBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.entity.FoodCollectionBean
import com.shuwei.dish.match.entity.GoodsItem
@@ -42,9 +46,11 @@ import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import androidx.core.content.withStyledAttributes
@SuppressLint("NotifyDataSetChanged")
class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>() {
@@ -63,30 +69,15 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
}
}
private val searchList = mutableListOf<GoodsItem>().apply {
add(GoodsItem(goodsId = "200001", goodsName = "土豆丝"))
add(GoodsItem(goodsId = "200002", goodsName = "土豆片"))
add(GoodsItem(goodsId = "200003", goodsName = "土豆丁"))
add(GoodsItem(goodsId = "200004", goodsName = "胡萝卜丝"))
add(GoodsItem(goodsId = "200005", goodsName = "胡萝卜片"))
add(GoodsItem(goodsId = "200006", goodsName = "胡萝卜丁"))
add(GoodsItem(goodsId = "200007", goodsName = "黄瓜丝"))
add(GoodsItem(goodsId = "200008", goodsName = "黄瓜片"))
add(GoodsItem(goodsId = "200009", goodsName = "黄瓜丁"))
add(GoodsItem(goodsId = "200010", goodsName = "洋葱丝"))
add(GoodsItem(goodsId = "200011", goodsName = "洋葱丁"))
}
private val searchList = mutableListOf<GoodsItem>()
private var checkedItem: GoodsItem? = null
private val searchAdapter by lazy {
Food3Adapter(searchList).apply {
setOnItemClickListener { adapter, view, position ->
searchList.forEachIndexed { index, item -> item.isClicked = index == position }
checkedItem = searchList[position]
notifyDataSetChanged()
checkedItem?.let {
selectedFoodId = it.goodsId
selectedFoodName = it.goodsName
}
setOnItemClickListener { _, _, position ->
val item = searchList[position]
val types = item.rawMaterialsTypes
// 弹出净材种类单选弹窗
showRawMaterialsDialog(item, position, types)
}
}
}
@@ -340,13 +331,143 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
@SuppressLint("NotifyDataSetChanged")
fun searchFood() {
debouncer.debounce {
searchByFoodName(binding.editFoodName.text.toString()) {
// searchFoodList.clear()
// searchFoodList.addAll(it)
// searchFoodAdapter.notifyDataSetChanged()
val keyword = binding.editFoodName.text.toString().trim()
// 通过 NetViewModel 查询净材数据(goodsType=0 食材)
currentActivity.netViewModel.queryGoodsList(
goodsType = "0",
goodsName = keyword.ifEmpty { null }
)
// 监听查询结果(一次性收集,避免重复订阅)
lifecycleScope.launch {
currentActivity.netViewModel.goodsListState.collect { state ->
when (state) {
is UiState.Success -> {
var list: List<GoodsItem> = state.data ?: emptyList()
// TODO: 接口暂无数据时使用假数据进行效果测试,联调完成后删除
if (list.isEmpty()) {
list = mutableListOf(
GoodsItem(goodsId = "1", goodsName = "土豆", rawMaterialsType = "土豆丝,土豆条,土豆片,土豆丁"),
GoodsItem(goodsId = "2", goodsName = "胡萝卜", rawMaterialsType = "胡萝卜丝,胡萝卜片,胡萝卜丁"),
GoodsItem(goodsId = "3", goodsName = "黄瓜", rawMaterialsType = "黄瓜丝,黄瓜片,黄瓜丁,黄瓜块"),
GoodsItem(goodsId = "4", goodsName = "洋葱", rawMaterialsType = "洋葱丝,洋葱丁,洋葱圈"),
GoodsItem(goodsId = "5", goodsName = "白菜", rawMaterialsType = "白菜丝,白菜块,白菜叶"),
GoodsItem(goodsId = "6", goodsName = "豆腐", rawMaterialsType = "豆腐块,豆腐丁"),
GoodsItem(goodsId = "7", goodsName = "猪肉", rawMaterialsType = "猪肉丝,猪肉片,猪肉丁,猪肉块"),
GoodsItem(goodsId = "8", goodsName = "鸡胸肉", rawMaterialsType = "鸡胸肉丝,鸡胸肉片,鸡胸肉丁"),
GoodsItem(goodsId = "9", goodsName = "牛肉", rawMaterialsType = "牛肉丝,牛肉片,牛肉块"),
GoodsItem(goodsId = "10", goodsName = "虾仁", rawMaterialsType = "整虾仁,切段虾仁"),
GoodsItem(goodsId = "11", goodsName = "茄子", rawMaterialsType = "茄子丝,茄子片,茄子块,茄子条"),
GoodsItem(goodsId = "12", goodsName = "青椒", rawMaterialsType = "青椒丝,青椒片,青椒块"),
GoodsItem(goodsId = "13", goodsName = "西红柿", rawMaterialsType = "西红柿片,西红柿块,西红柿丁"),
GoodsItem(goodsId = "14", goodsName = "芹菜", rawMaterialsType = "芹菜段,芹菜丝"),
GoodsItem(goodsId = "15", goodsName = "莲藕", rawMaterialsType = "莲藕片,莲藕丁,莲藕丝"),
// 以下5条 rawMaterialsType 为空,测试无净材数据场景
GoodsItem(goodsId = "16", goodsName = "菠菜"),
GoodsItem(goodsId = "17", goodsName = "生菜"),
GoodsItem(goodsId = "18", goodsName = "香菇"),
GoodsItem(goodsId = "19", goodsName = "木耳"),
GoodsItem(goodsId = "20", goodsName = "豆芽"),
)
}
// 将 rawMaterialsType 转存到 rawMaterialsTypes,并清空 rawMaterialsType
list.forEach { item ->
item.rawMaterialsTypes = item.rawMaterialsType
item.rawMaterialsType = null
}
searchList.clear()
searchList.addAll(list)
searchAdapter.notifyDataSetChanged()
}
else -> Unit
}
}
}
}
}
/**
* 弹出净材种类单选弹窗
* @param item 当前点击的食材项
* @param position 在 searchList 中的位置
* @param types 净材种类字符串(逗号分隔),为空时提示无数据
*/
@SuppressLint("NotifyDataSetChanged")
private fun showRawMaterialsDialog(item: GoodsItem, position: Int, types: String?) {
val typeList = types?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
val dialog = CommonDialog(requireContext())
.setTitle("选择净材种类")
.setNegativeButton("取消")
if (typeList.isEmpty()) {
// 无净材数据时仅展示提示文字
dialog.setContent("无净材数据")
.setPositiveButton("确认") {}
} else {
// 构建单选 RadioGroup 作为自定义内容
val radioGroup = RadioGroup(requireContext()).apply {
orientation = RadioGroup.VERTICAL
// RadioGroup 本身居中,子项 wrap_content 宽度,整体图标+文字自然居中
gravity = Gravity.CENTER_HORIZONTAL
typeList.forEachIndexed { index, typeName ->
val rb = RadioButton(requireContext()).apply {
id = index
text = typeName
textSize = 26f
layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.WRAP_CONTENT,
RadioGroup.LayoutParams.WRAP_CONTENT
)
setPadding(30.dp, 15.dp, 30.dp, 15.dp)
// 移除默认点击水波纹背景,改用 foreground 实现 selectableItemBackground 效果
setBackgroundResource(0)
val attrs = intArrayOf(android.R.attr.selectableItemBackground)
context.withStyledAttributes(null, attrs) {
foreground = getDrawable(0)
}
// 若当前项已有选中值则默认勾选对应选项
if (typeName == item.rawMaterialsType) isChecked = true
// 通过 InsetDrawable 在 buttonDrawable 右侧追加 80dp 间距,实现图标与文字的间距控制
buttonDrawable?.let { orig ->
buttonDrawable = android.graphics.drawable.InsetDrawable(
orig, 30.dp, 0, 30.dp, 0
)
}
}
addView(rb)
}
}
dialog.addContentView(radioGroup) { tvContent ->
tvContent.gone()
}
dialog.setPositiveButtonInterceptable("确认") {
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId == -1) {
toast("请选择净材种类")
// 返回 false 阻止弹窗关闭
return@setPositiveButtonInterceptable false
}
val selectedType = typeList[checkedId]
// 清空其他已选项的状态
searchList.forEachIndexed { index, it ->
if (index != position) {
it.isClicked = false
it.rawMaterialsType = null
}
}
// 设置当前选中项
item.isClicked = true
item.rawMaterialsType = selectedType
checkedItem = item
selectedFoodId = item.goodsId
selectedFoodName = item.goodsName
searchAdapter.notifyDataSetChanged()
true
}
}
dialog.show()
}
var clickIndex = -1
@@ -391,7 +512,6 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
LogSaveUtil.saveLogFile(msg)
}
private fun searchByFoodName(name: String, block: (String) -> Unit) {}
private fun hideWaitingDialog() {
currentActivity.dismissLoading()
}
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- 绿色圆角背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png -->
<?xml version="1.0" encoding="utf-8"?>
<!-- 绿色圆形背景 + 白色垃圾桶图标,替代红色的 ic_delete_red.png -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 绿色圆角背景 -->
@@ -6,10 +7,10 @@
<shape android:shape="oval">
<solid android:color="#00BC71" />
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</shape>
</item>
+10
View File
@@ -37,6 +37,16 @@
android:textSize="26sp"
tools:text="这是弹窗的内容信息,请确认"/>
<!-- 自定义内容区域,默认隐藏,通过 CommonDialog.addContentView 动态填充 -->
<LinearLayout
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:orientation="vertical"
android:visibility="gone" />
<!-- 分割线 -->
<View
android:layout_width="match_parent"
@@ -18,6 +18,7 @@
android:layout_gravity="center"
android:text="点击分格配置调料"
android:includeFontPadding="false"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="20sp" />
</FrameLayout>
+26 -6
View File
@@ -1,18 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:background="@drawable/bg_item_search"
android:paddingHorizontal="15dp"
android:paddingVertical="10dp">
<!-- 食材名称,选中状态通过 CheckedTextView 的 checked 驱动背景/文字色变化 -->
<CheckedTextView
android:id="@+id/tvFoodName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:layout_marginHorizontal="14dp"
android:layout_marginVertical="14dp"
android:paddingHorizontal="15dp"
android:textAlignment="center"
android:textColor="@color/color_item_search"
android:textSize="30sp"
android:textSize="28sp"
android:textStyle="bold"
tools:text="土豆丝" />
<!-- 当前选中的净材种类,小字显示,无数据时隐藏 -->
<TextView
android:id="@+id/tvRawMaterialsType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center|bottom"
android:textColor="@color/black999"
android:textSize="18sp"
android:visibility="gone"
tools:text="切丝" />
</FrameLayout>