feat(form): 实现餐品净菜包装表单功能

- 新增 CHILDREN 字段类型支持动态子列表
- 实现包含食材的嵌套表单结构
- 添加餐品包装相关字段配置
- 集成下拉选项和日期选择器组件
- 创建食材行适配器处理子列表渲染
- 更新表单布局和样式资源文件
This commit is contained in:
2026-06-01 14:18:16 +08:00
parent c4a2105737
commit 5d6df58e46
11 changed files with 598 additions and 15 deletions
@@ -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 中,无需额外操作
}
)
}
}
@@ -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<MutableList<FormField>>,
private val onDelete: (Int) -> Unit,
private val onChildClick: (Int, Int, FormField) -> Unit
) : BaseQuickAdapter<MutableList<FormField>, 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<FormField>?) {
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)
}
}
}
@@ -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)
}
@@ -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<FormField>
*/
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<String, String> = mutableMapOf()
val extraValues: MutableMap<String, String> = mutableMapOf(),
val children: List<FormField>? = null,
val childAddLabel: String? = null,
val childrenRows: MutableList<MutableList<FormField>> = mutableListOf()
)
@@ -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 })
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="#FFDCDCF0" android:width="1dp"/>
</shape>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white"/>
<stroke android:color="#FF0032C8" android:width="2dp"/>
<corners android:radius="10dp"/>
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#30000000">
<item>
<shape android:shape="rectangle">
<solid android:color="#99FFFFFF"/>
</shape>
</item>
</ripple>
@@ -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">
<TextView
android:id="@+id/tvDropdown"
@@ -4,7 +4,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="10dp">
android:paddingVertical="10dp"
tools:background="@color/white">
<!-- 字段标签,必填项红色星号由 Adapter 通过 SpannableString 追加 -->
<TextView
@@ -18,11 +19,135 @@
android:textStyle="bold"
tools:text="食材名称" />
<!-- CHILDREN 类型:子列表容器 -->
<LinearLayout
android:id="@+id/llChildren"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
<!-- 子列表标题行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical"
android:background="@drawable/bg_rect_header">
<!-- 食材溯源码表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginEnd="2dp"
android:gravity="center"
android:text="食材溯源码"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 食材名称表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginHorizontal="2dp"
android:gravity="center"
android:text="食材名称"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 分类表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginHorizontal="2dp"
android:gravity="center"
android:text="分类"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 每份用量表头 -->
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:layout_marginStart="2dp"
android:maxLines="1"
android:gravity="center"
android:text="每份用量"
android:textColor="#FF141428"
android:textSize="16sp" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#FFDCDCF0"/>
<!-- 操作表头 -->
<TextView
android:layout_width="50dp"
android:layout_height="match_parent"
android:text="操作"
android:gravity="center"
android:textColor="#FF141428"
android:textSize="16sp" />
</LinearLayout>
<!-- 嵌套 RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvChildren"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
tools:listitem="@layout/list_item_ingredient_row"/>
<!-- 添加按钮 -->
<TextView
android:id="@+id/tvAddChild"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:paddingVertical="5dp"
android:paddingStart="5dp"
android:paddingEnd="10dp"
android:textColor="#FF0066CC"
android:textSize="18sp"
android:textStyle="bold"
android:text="+ 添加食材"/>
</LinearLayout>
<!-- 输入区域容器,三种控件叠放,按 FieldType 切换 visibility -->
<FrameLayout
android:id="@+id/flInputArea"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginHorizontal="12dp">
android:layout_marginHorizontal="12dp"
android:visibility="gone">
<!-- TEXT / NUMBER / FIXED 类型:EditText -->
<EditText
@@ -0,0 +1,113 @@
<?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:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp"
tools:ignore="HardcodedText">
<!-- 食材溯源码 -->
<EditText
android:id="@+id/etTraceCode"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2"
android:hint="溯源码"
android:paddingHorizontal="10dp"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp"
tools:ignore="Autofill,TextFields" />
<!-- 食材名称 -->
<FrameLayout
android:id="@+id/flMaterName"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2">
<TextView
android:id="@+id/tvMaterName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="24dp"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp" />
<ImageView
android:layout_width="18dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="6dp"
android:src="@mipmap/ic_triangle_down"
tools:ignore="ContentDescription" />
</FrameLayout>
<!-- 分类 -->
<FrameLayout
android:id="@+id/flClassify"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_white_radius10_stroke2">
<TextView
android:id="@+id/tvClassify"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="24dp"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp" />
<ImageView
android:layout_width="18dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="6dp"
android:src="@mipmap/ic_triangle_down"
tools:ignore="ContentDescription" />
</FrameLayout>
<!-- 每份用量 -->
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/bg_white_radius10_stroke2"
android:hint="如 120g"
android:paddingHorizontal="10dp"
android:textColor="#FF141428"
android:textColorHint="#FF96A0AA"
android:textSize="16sp"
tools:ignore="Autofill,TextFields" />
<!-- 删除按钮 -->
<TextView
android:id="@+id/tvRemove"
android:layout_width="50dp"
android:layout_height="50dp"
android:gravity="center"
android:text="×"
android:textColor="#FF96A0AA"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>