From ef13aa8307aab006c21754172c15d25d82f90f88 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Sat, 9 May 2026 18:10:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(fragment):=20=E6=96=B0=E5=A2=9E=E5=87=80?= =?UTF-8?q?=E6=9D=90=E7=A7=8D=E7=B1=BB=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 rawMaterialsType 字段至 GoodsItem、CookFoodGoodsEntity、SeasoningEntity,DB 升级至 v12 - VectorCollectionFragment 重构食材搜索,点击食材弹出净材种类单选弹窗 - CommonDialog 新增 addContentView 和可拦截确认按钮(setPositiveButtonInterceptable)能力 - Food3Adapter 和 list_item_food3.xml 支持显示已选净材种类副标签 --- .../shuwei/dish/match/adapter/Food3Adapter.kt | 10 + .../com/shuwei/dish/match/db/AppDatabase.kt | 11 +- .../shuwei/dish/match/dialog/CommonDialog.kt | 43 ++++- .../dish/match/entity/CookFoodGoodsEntity.kt | 6 +- .../com/shuwei/dish/match/entity/GoodsItem.kt | 12 +- .../dish/match/entity/SeasoningEntity.kt | 3 + .../ui/fragment/VectorCollectionFragment.kt | 172 +++++++++++++++--- app/src/main/res/drawable/ic_delete_green.xml | 11 +- app/src/main/res/layout/dialog_common.xml | 10 + .../res/layout/fragment_seasoning_config.xml | 1 + app/src/main/res/layout/list_item_food3.xml | 42 +++-- 11 files changed, 268 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/Food3Adapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/Food3Adapter.kt index 4876190..dfdebf4 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/Food3Adapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/Food3Adapter.kt @@ -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) : 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( diff --git a/app/src/main/java/com/shuwei/dish/match/db/AppDatabase.kt b/app/src/main/java/com/shuwei/dish/match/db/AppDatabase.kt index fc73e3e..41fc0bc 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/AppDatabase.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/AppDatabase.kt @@ -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() } } 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 3247361..c198ce7 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 @@ -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() { diff --git a/app/src/main/java/com/shuwei/dish/match/entity/CookFoodGoodsEntity.kt b/app/src/main/java/com/shuwei/dish/match/entity/CookFoodGoodsEntity.kt index fbc4326..79f0547 100644 --- a/app/src/main/java/com/shuwei/dish/match/entity/CookFoodGoodsEntity.kt +++ b/app/src/main/java/com/shuwei/dish/match/entity/CookFoodGoodsEntity.kt @@ -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 ) } \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/entity/GoodsItem.kt b/app/src/main/java/com/shuwei/dish/match/entity/GoodsItem.kt index afbadd4..c0d574c 100644 --- a/app/src/main/java/com/shuwei/dish/match/entity/GoodsItem.kt +++ b/app/src/main/java/com/shuwei/dish/match/entity/GoodsItem.kt @@ -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 ) } diff --git a/app/src/main/java/com/shuwei/dish/match/entity/SeasoningEntity.kt b/app/src/main/java/com/shuwei/dish/match/entity/SeasoningEntity.kt index 60a3eec..7d13025 100644 --- a/app/src/main/java/com/shuwei/dish/match/entity/SeasoningEntity.kt +++ b/app/src/main/java/com/shuwei/dish/match/entity/SeasoningEntity.kt @@ -61,6 +61,9 @@ data class SeasoningEntity( /** 物料编码 */ var goodsCode: String? = "", + /** 净材种类 */ + var rawMaterialsType: String? = null, + var sort: Int = 9999, /** * isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据 diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt index 4c930d9..c7a1fde 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt @@ -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() { @@ -63,30 +69,15 @@ class VectorCollectionFragment : BaseFragment() } } - private val searchList = mutableListOf().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() 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,14 +331,144 @@ class VectorCollectionFragment : BaseFragment() @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 = 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 @SuppressLint("NotifyDataSetChanged") @@ -391,7 +512,6 @@ class VectorCollectionFragment : BaseFragment() LogSaveUtil.saveLogFile(msg) } - private fun searchByFoodName(name: String, block: (String) -> Unit) {} private fun hideWaitingDialog() { currentActivity.dismissLoading() } diff --git a/app/src/main/res/drawable/ic_delete_green.xml b/app/src/main/res/drawable/ic_delete_green.xml index ce74e7a..1474e33 100644 --- a/app/src/main/res/drawable/ic_delete_green.xml +++ b/app/src/main/res/drawable/ic_delete_green.xml @@ -1,4 +1,5 @@ - + + @@ -6,10 +7,10 @@ + android:bottom="8dp" + android:left="8dp" + android:right="8dp" + android:top="8dp" /> diff --git a/app/src/main/res/layout/dialog_common.xml b/app/src/main/res/layout/dialog_common.xml index e0c51c8..e4fd477 100644 --- a/app/src/main/res/layout/dialog_common.xml +++ b/app/src/main/res/layout/dialog_common.xml @@ -37,6 +37,16 @@ android:textSize="26sp" tools:text="这是弹窗的内容信息,请确认"/> + + + diff --git a/app/src/main/res/layout/list_item_food3.xml b/app/src/main/res/layout/list_item_food3.xml index 6999911..3b1a84f 100644 --- a/app/src/main/res/layout/list_item_food3.xml +++ b/app/src/main/res/layout/list_item_food3.xml @@ -1,18 +1,38 @@ - \ No newline at end of file + android:paddingVertical="10dp"> + + + + + + + +