From 5d6df58e4645010a7b1acc89ca42aea78dfa5861 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Mon, 1 Jun 2026 14:18:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(form):=20=E5=AE=9E=E7=8E=B0=E9=A4=90?= =?UTF-8?q?=E5=93=81=E5=87=80=E8=8F=9C=E5=8C=85=E8=A3=85=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CHILDREN 字段类型支持动态子列表 - 实现包含食材的嵌套表单结构 - 添加餐品包装相关字段配置 - 集成下拉选项和日期选择器组件 - 创建食材行适配器处理子列表渲染 - 更新表单布局和样式资源文件 --- .../dish/match/adapter/FormFieldAdapter.kt | 62 ++++++++ .../match/adapter/IngredientRowAdapter.kt | 135 +++++++++++++++++ .../shuwei/dish/match/dialog/DropdownPopup.kt | 2 +- .../com/shuwei/dish/match/model/FormField.kt | 12 +- .../shuwei/dish/match/ui/CleanPackActivity.kt | 138 +++++++++++++++++- app/src/main/res/drawable/bg_rect_header.xml | 5 + .../res/drawable/bg_white_stroke_blue.xml | 6 - .../res/drawable/ripple_effect_light3.xml | 9 ++ .../main/res/layout/list_item_dropdown.xml | 2 +- .../main/res/layout/list_item_form_field.xml | 129 +++++++++++++++- .../res/layout/list_item_ingredient_row.xml | 113 ++++++++++++++ 11 files changed, 598 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt create mode 100644 app/src/main/res/drawable/bg_rect_header.xml delete mode 100644 app/src/main/res/drawable/bg_white_stroke_blue.xml create mode 100644 app/src/main/res/drawable/ripple_effect_light3.xml create mode 100644 app/src/main/res/layout/list_item_ingredient_row.xml diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt index 77e5bea..7626cc5 100644 --- a/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt @@ -11,6 +11,7 @@ import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.graphics.toColorInt +import androidx.recyclerview.widget.LinearLayoutManager import com.chad.library.adapter4.BaseQuickAdapter import com.chad.library.adapter4.viewholder.QuickViewHolder import com.shuwei.dish.match.R @@ -57,9 +58,11 @@ class FormFieldAdapter( b.tvLabel.text = buildLabelSpannable(item.label, item.required) // 先全部隐藏,再按类型显示对应控件 + b.flInputArea.gone() b.etInput.gone() b.flDropdown.gone() b.flDatePicker.gone() + b.llChildren.gone() // 移除旧的 TextWatcher,防止 RecyclerView 复用时错误触发 (b.etInput.tag as? TextWatcher)?.let { b.etInput.removeTextChangedListener(it) } @@ -75,6 +78,15 @@ class FormFieldAdapter( FieldType.FIXED -> bindFixed(b, item) FieldType.DROPDOWN -> bindDropdown(b, item) FieldType.DATE_PICKER -> bindDatePicker(b, item) + FieldType.CHILDREN -> bindChildren(b, item) + } + + if (item.type == FieldType.CHILDREN) { + b.flInputArea.gone() + b.llChildren.visible() + } else { + b.flInputArea.visible() + b.llChildren.gone() } } @@ -189,4 +201,54 @@ class FormFieldAdapter( } } + /** 绑定 CHILDREN 类型:渲染子列表(表头 + 嵌套 RecyclerView + 添加按钮) */ + private fun bindChildren(b: ListItemFormFieldBinding, item: FormField) { + b.llChildren.visible() + b.tvAddChild.text = item.childAddLabel ?: "+ 添加" + + // 初始化嵌套 RecyclerView + if (b.rvChildren.adapter == null) { + b.rvChildren.layoutManager = LinearLayoutManager(context) + } + b.rvChildren.adapter = createRowAdapter(b, item) + + // 添加按钮点击 + b.tvAddChild.setOnClickListener { + item.children?.let { template -> + val newRow = template.map { field -> + FormField( + label = field.label, + type = field.type, + apiKey = field.apiKey, + extraApiKeys = field.extraApiKeys, + required = field.required, + hint = field.hint, + options = field.options, + submitValueId = field.submitValueId + ) + }.toMutableList() + item.childrenRows.add(newRow) + b.rvChildren.adapter?.notifyItemInserted(item.childrenRows.size - 1) + } + } + } + + /** 创建子列表行 Adapter */ + private fun createRowAdapter(b: ListItemFormFieldBinding, item: FormField): IngredientRowAdapter { + return IngredientRowAdapter( + rows = item.childrenRows, + onDelete = onDelete@{ position -> + val adapter = b.rvChildren.adapter as? IngredientRowAdapter ?: return@onDelete + item.childrenRows.removeAt(position) + adapter.notifyItemRemoved(position) + if (position < item.childrenRows.size) { + adapter.notifyItemRangeChanged(position, item.childrenRows.size - position) + } + }, + onChildClick = { _, _, _ -> + // 子字段值已直接更新到 row 的 FormField 中,无需额外操作 + } + ) + } + } diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt new file mode 100644 index 0000000..16cc48b --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/adapter/IngredientRowAdapter.kt @@ -0,0 +1,135 @@ +package com.shuwei.dish.match.adapter + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.shuwei.dish.match.R +import com.shuwei.dish.match.databinding.ListItemIngredientRowBinding +import com.shuwei.dish.match.dialog.DropdownPopup +import com.shuwei.dish.match.model.FormField +import com.shuwei.dish.match.utils.ext.dp +import com.shuwei.dish.match.utils.ext.hideKeyboard + +/** + * 食材行 Adapter,用于「包含食材」子列表的嵌套渲染 + * @param context Activity 上下文 + * @param rows 每行包含多个 FormField,对应溯源码、食材名称、分类、用量 + * @param onDelete 删除行回调 + * @param onChildClick 子字段点击回调(如下拉框点击) + */ +class IngredientRowAdapter( + rows: MutableList>, + private val onDelete: (Int) -> Unit, + private val onChildClick: (Int, Int, FormField) -> Unit +) : BaseQuickAdapter, IngredientRowAdapter.VH>(rows) { + + inner class VH(val binding: ListItemIngredientRowBinding) : QuickViewHolder(binding.root) + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val binding = ListItemIngredientRowBinding.inflate(LayoutInflater.from(context), parent, false) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int, item: MutableList?) { + item ?: return + val b = holder.binding + + // 假设 children 模板顺序:[溯源码(TEXT), 食材名称(DROPDOWN), 分类(DROPDOWN), 用量(TEXT)] + val traceCodeField = item.getOrNull(0) + val materNameField = item.getOrNull(1) + val classifyField = item.getOrNull(2) + val amountField = item.getOrNull(3) + + // 溯源码 - TEXT + traceCodeField?.let { field -> + b.etTraceCode.setText(field.value) + b.etTraceCode.hint = field.hint.ifBlank { field.label } + (b.etTraceCode.tag as? TextWatcher)?.let { b.etTraceCode.removeTextChangedListener(it) } + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + field.value = s?.toString() ?: "" + } + } + b.etTraceCode.addTextChangedListener(watcher) + b.etTraceCode.tag = watcher + } + + // 食材名称 - DROPDOWN + materNameField?.let { field -> + b.tvMaterName.hint = field.hint.ifBlank { "请选择${field.label}" } + b.tvMaterName.text = field.value.ifBlank { null } + b.flMaterName.setOnClickListener { v -> + v.hideKeyboard() + showDropdown(b.flMaterName, field, b.tvMaterName, holder.getAdapterPosition(), 1) + } + } + + // 分类 - DROPDOWN + classifyField?.let { field -> + b.tvClassify.hint = field.hint.ifBlank { "请选择${field.label}" } + b.tvClassify.text = field.value.ifBlank { null } + b.flClassify.setOnClickListener { v -> + v.hideKeyboard() + showDropdown(b.flClassify, field, b.tvClassify, holder.getAdapterPosition(), 2) + } + } + + // 用量 - TEXT + amountField?.let { field -> + b.etAmount.setText(field.value) + b.etAmount.hint = field.hint.ifBlank { field.label } + (b.etAmount.tag as? TextWatcher)?.let { b.etAmount.removeTextChangedListener(it) } + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + field.value = s?.toString() ?: "" + } + } + b.etAmount.addTextChangedListener(watcher) + b.etAmount.tag = watcher + } + + // 删除按钮 + b.tvRemove.setOnClickListener { + val pos = holder.layoutPosition + if (pos != RecyclerView.NO_POSITION) { + onDelete(pos) + } + } + } + + /** 显示下拉弹出框 */ + private fun showDropdown( + anchorView: ViewGroup, + field: FormField, + valueView: TextView, + rowPosition: Int, + childIndex: Int + ) { + DropdownPopup( + context = context, + list = field.options.toMutableList(), + popWidth = anchorView.width, + popHeight = kotlin.math.min(field.options.size.coerceAtLeast(1), 4) * 50.dp + ) { dictType -> + field.value = dictType.value ?: "" + field.valueId = dictType.id ?: "" + valueView.text = field.value + // 回调通知外部更新 + onChildClick(rowPosition, childIndex, field) + }.also { + it.bgLayout = anchorView + it.showAsDropDown(anchorView) + } + } + +} diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt b/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt index 611e3d6..42c9086 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt @@ -65,7 +65,7 @@ class DropdownPopup( override fun showAsDropDown(anchor: View?) { super.showAsDropDown(anchor) - bgLayout?.setBackgroundResource(R.drawable.bg_white_stroke_blue) + bgLayout?.setBackgroundResource(R.drawable.shape_green_stroke) arrowImage?.setImageResource(R.mipmap.ic_triangle_up) } diff --git a/app/src/main/java/com/shuwei/dish/match/model/FormField.kt b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt index 105eb86..16b0eed 100644 --- a/app/src/main/java/com/shuwei/dish/match/model/FormField.kt +++ b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt @@ -13,7 +13,9 @@ enum class FieldType { /** 日期选择弹窗 */ DATE_PICKER, /** 固定值,不可编辑 */ - FIXED + FIXED, + /** 子列表容器,用于动态增减行(如"包含食材") */ + CHILDREN } /** @@ -30,6 +32,9 @@ enum class FieldType { * @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式 * @param valueId DATE_PICKER 存 ISO 格式 yyyy-MM-dd'T'HH:mm:ss * @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名 + * @param children 子字段模板(CHILDREN 类型使用),每行按此模板创建一组字段 + * @param childAddLabel 添加按钮文案(CHILDREN 类型使用),如 "+ 添加食材" + * @param childrenRows 子列表实际数据(CHILDREN 类型使用),每行是一个 List */ data class FormField( val label: String, @@ -43,5 +48,8 @@ data class FormField( val submitValueId: Boolean = false, var value: String = "", var valueId: String = "", - val extraValues: MutableMap = mutableMapOf() + val extraValues: MutableMap = mutableMapOf(), + val children: List? = null, + val childAddLabel: String? = null, + val childrenRows: MutableList> = mutableListOf() ) diff --git a/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt index 3bab141..60197fb 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/CleanPackActivity.kt @@ -9,6 +9,7 @@ import com.shuwei.dish.match.R import com.shuwei.dish.match.adapter.FormFieldAdapter import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.databinding.ActivityCleanPackBinding +import com.shuwei.dish.match.model.DictType import com.shuwei.dish.match.model.FieldType import com.shuwei.dish.match.model.FormField import com.shuwei.dish.match.utils.ext.dp @@ -170,12 +171,143 @@ class CleanPackActivity : BaseActivity() { /** * 初始化餐品净菜包装表单字段 - * TODO: 由用户提供具体字段数据 + * 对应接口 NutSupMealPackageDTO */ private fun initMealPackFields() { fields.clear() - // 待填充 - formAdapter.submitList(fields) + + // 包装餐品 + fields.add( + FormField( + label = "包装餐品", type = FieldType.DROPDOWN, + apiKey = "foodId", required = true, + options = listOf( + DictType(id = "1", value = "1", type = "宫保鸡丁"), + DictType(id = "2", value = "2", type = "鱼香肉丝"), + DictType(id = "3", value = "3", type = "红烧茄子"), + ) + ) + ) + + // 包装规格 + fields.add( + FormField( + label = "包装规格", type = FieldType.DROPDOWN, + apiKey = "spec", required = true, + options = listOf( + DictType(id = "1", value = "500g/盒", type = "500g/盒"), + DictType(id = "2", value = "1kg/盒", type = "1kg/盒"), + ) + ) + ) + + // 包装方式 + fields.add( + FormField( + label = "包装方式", type = FieldType.DROPDOWN, + apiKey = "packageMethod", required = true, + options = listOf( + DictType(id = "1", value = "真空包装", type = "真空包装"), + DictType(id = "2", value = "气调包装", type = "气调包装"), + ) + ) + ) + + // 包装份数 + fields.add( + FormField( + label = "包装份数", type = FieldType.INTEGER, + apiKey = "quantity", required = true, hint = "请输入份数" + ) + ) + + // 包含食材(子列表容器) + val ingredientTemplate = listOf( + FormField(label = "食材溯源码", type = FieldType.TEXT, required = true, hint = "溯源码"), + FormField( + label = "食材名称", type = FieldType.DROPDOWN, required = true, + options = listOf( + DictType(id = "1", value = "土豆", type = "土豆"), + DictType(id = "2", value = "鸡肉", type = "鸡肉"), + DictType(id = "3", value = "青椒", type = "青椒"), + ) + ), + FormField( + label = "分类", type = FieldType.DROPDOWN, required = true, + options = listOf( + DictType(id = "1", value = "蔬菜类", type = "蔬菜类"), + DictType(id = "2", value = "肉禽类", type = "肉禽类"), + DictType(id = "3", value = "水产类", type = "水产类"), + ) + ), + FormField(label = "每份用量", type = FieldType.TEXT, required = true, hint = "如 120g"), + ) + fields.add( + FormField( + label = "包含食材", type = FieldType.CHILDREN, + required = true, + children = ingredientTemplate, + childAddLabel = "+ 添加食材" + ) + ) + + // 包装日期 + fields.add( + FormField( + label = "包装日期", type = FieldType.DATE_PICKER, + apiKey = "packageDate", required = true + ) + ) + + // 保质期至 + fields.add( + FormField( + label = "保质期至", type = FieldType.DATE_PICKER, + apiKey = "expiryDate", required = true + ) + ) + + // 存放温度 + fields.add( + FormField( + label = "存放温度(℃)", type = FieldType.DECIMAL, + apiKey = "storageTemp", required = false, hint = "如 3.8" + ) + ) + + // 包装人 + fields.add( + FormField( + label = "包装人", type = FieldType.TEXT, + apiKey = "operator", required = false, hint = "请输入包装人" + ) + ) + + // 终端设备 + fields.add( + FormField( + label = "终端设备", type = FieldType.DROPDOWN, + apiKey = "deviceId", required = false, + options = listOf( + DictType(id = "1", value = "1", type = "包装机-01"), + DictType(id = "2", value = "2", type = "包装机-02"), + ) + ) + ) + + // 操作类型 + fields.add( + FormField( + label = "操作类型", type = FieldType.DROPDOWN, + apiKey = "opType", required = true, + options = listOf( + DictType(id = "1", value = "1", type = "设备自动"), + DictType(id = "2", value = "2", type = "人工录入"), + ) + ) + ) + + formAdapter.submitList(fields.filter { !it.hidden }) } } diff --git a/app/src/main/res/drawable/bg_rect_header.xml b/app/src/main/res/drawable/bg_rect_header.xml new file mode 100644 index 0000000..6e2859c --- /dev/null +++ b/app/src/main/res/drawable/bg_rect_header.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_white_stroke_blue.xml b/app/src/main/res/drawable/bg_white_stroke_blue.xml deleted file mode 100644 index b185704..0000000 --- a/app/src/main/res/drawable/bg_white_stroke_blue.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ripple_effect_light3.xml b/app/src/main/res/drawable/ripple_effect_light3.xml new file mode 100644 index 0000000..8976f29 --- /dev/null +++ b/app/src/main/res/drawable/ripple_effect_light3.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/layout/list_item_dropdown.xml b/app/src/main/res/layout/list_item_dropdown.xml index 95f1d19..4c38db7 100644 --- a/app/src/main/res/layout/list_item_dropdown.xml +++ b/app/src/main/res/layout/list_item_dropdown.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:background="@drawable/ripple_effect_light"> + android:background="@drawable/ripple_effect_light3"> + android:paddingVertical="10dp" + tools:background="@color/white"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_marginHorizontal="12dp" + android:visibility="gone"> + + + + + + + + + + + + + + + + + + + + + + + + + + + +