feat(form): 重构净菜包装表单实现动态字段和提交功能

- 移除固定 DTO 类型,改用 Map<String, Any> 接收 API 参数
- 实现动态表单字段配置,支持净菜和餐品两种包装类型
- 添加下拉选项动态加载和自动填充溯源码功能
- 实现表单验证和通用提交逻辑
- 添加回调版本的数据加载方法提高性能
- 优化字段绑定和值映射机制
This commit is contained in:
2026-06-01 16:51:47 +08:00
parent 5d6df58e46
commit 4a2abf6eec
9 changed files with 450 additions and 101 deletions
@@ -137,18 +137,11 @@ class FormFieldAdapter(
popHeight = popHeight popHeight = popHeight
) { dictType -> ) { dictType ->
item.value = dictType.value ?: "" item.value = dictType.value ?: ""
item.valueId = dictType.id ?: ""
b.tvDropdownValue.text = item.value b.tvDropdownValue.text = item.value
// 将 extraApiKeys 映射的 DictType 字段值写入 extraValues // 将 extraApiKeys 映射的 DictType 字段值写入 extraValues
item.extraValues.clear() item.extraValues.clear()
item.extraApiKeys.forEach { (apiKey, dictField) -> item.extraApiKeys.forEach { (apiKey, dictField) ->
val fieldValue = when (dictField) { item.extraValues[apiKey] = dictType[dictField]
"id" -> dictType.id ?: ""
"value" -> dictType.value ?: ""
"type" -> dictType.type
else -> ""
}
item.extraValues[apiKey] = fieldValue
} }
}.also { }.also {
it.bgLayout = b.flDropdown it.bgLayout = b.flDropdown
@@ -175,7 +168,6 @@ class FormFieldAdapter(
} }
// value 存显示格式,valueId 存 ISO 格式供提交使用 // value 存显示格式,valueId 存 ISO 格式供提交使用
item.value = displayDateFormat.format(selected.time) item.value = displayDateFormat.format(selected.time)
item.valueId = isoFormat.format(selected.time)
b.tvDateValue.text = item.value b.tvDateValue.text = item.value
}, },
cal.get(Calendar.YEAR), cal.get(Calendar.YEAR),
@@ -224,7 +216,6 @@ class FormFieldAdapter(
required = field.required, required = field.required,
hint = field.hint, hint = field.hint,
options = field.options, options = field.options,
submitValueId = field.submitValueId
) )
}.toMutableList() }.toMutableList()
item.childrenRows.add(newRow) item.childrenRows.add(newRow)
@@ -122,7 +122,6 @@ class IngredientRowAdapter(
popHeight = kotlin.math.min(field.options.size.coerceAtLeast(1), 4) * 50.dp popHeight = kotlin.math.min(field.options.size.coerceAtLeast(1), 4) * 50.dp
) { dictType -> ) { dictType ->
field.value = dictType.value ?: "" field.value = dictType.value ?: ""
field.valueId = dictType.id ?: ""
valueView.text = field.value valueView.text = field.value
// 回调通知外部更新 // 回调通知外部更新
onChildClick(rowPosition, childIndex, field) onChildClick(rowPosition, childIndex, field)
@@ -11,4 +11,14 @@ data class DictType(
val id: String? = null, val id: String? = null,
val value: String? = null, val value: String? = null,
val type: String = "" val type: String = ""
) : Parcelable ) : Parcelable {
/**
* 按字段名取值,供 extraApiKeys 映射直接使用
*/
operator fun get(fieldName: String): String = when (fieldName) {
"id" -> id ?: ""
"value" -> value ?: ""
else -> type
}
}
@@ -28,9 +28,7 @@ enum class FieldType {
* @param hidden 是否隐藏,hidden=true 时不渲染该字段 * @param hidden 是否隐藏,hidden=true 时不渲染该字段
* @param hint 校验失败提示文案,同时作为输入框 hint * @param hint 校验失败提示文案,同时作为输入框 hint
* @param options 下拉框选项列表(DROPDOWN 类型使用) * @param options 下拉框选项列表(DROPDOWN 类型使用)
* @param submitValueId 提交时使用 valueId 而非 valueDATE_PICKER 选 ISO 格式时设为 true
* @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式 * @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式
* @param valueId DATE_PICKER 存 ISO 格式 yyyy-MM-dd'T'HH:mm:ss
* @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名 * @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名
* @param children 子字段模板(CHILDREN 类型使用),每行按此模板创建一组字段 * @param children 子字段模板(CHILDREN 类型使用),每行按此模板创建一组字段
* @param childAddLabel 添加按钮文案(CHILDREN 类型使用),如 "+ 添加食材" * @param childAddLabel 添加按钮文案(CHILDREN 类型使用),如 "+ 添加食材"
@@ -44,10 +42,8 @@ data class FormField(
val required: Boolean = false, val required: Boolean = false,
val hidden: Boolean = false, val hidden: Boolean = false,
val hint: String = "", val hint: String = "",
val options: List<DictType> = emptyList(), var options: MutableList<DictType> = mutableListOf(),
val submitValueId: Boolean = false,
var value: String = "", var value: String = "",
var valueId: String = "",
val extraValues: MutableMap<String, String> = mutableMapOf(), val extraValues: MutableMap<String, String> = mutableMapOf(),
val children: List<FormField>? = null, val children: List<FormField>? = null,
val childAddLabel: String? = null, val childAddLabel: String? = null,
@@ -1,8 +1,6 @@
package com.shuwei.dish.match.net package com.shuwei.dish.match.net
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.model.NutSupCleanPackageDTO
import com.shuwei.dish.match.model.NutSupMealPackageDTO
import com.shuwei.dish.match.model.NutSupPkgDeviceVO import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import com.shuwei.dish.match.model.NutCanteenVO import com.shuwei.dish.match.model.NutCanteenVO
import com.shuwei.dish.match.model.NutDictItemVO import com.shuwei.dish.match.model.NutDictItemVO
@@ -123,7 +121,7 @@ interface ApiServiceV2 {
@POST @POST
suspend fun addCleanPackage( suspend fun addCleanPackage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/clean-package/add", @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/clean-package/add",
@Body param: NutSupCleanPackageDTO @Body param: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<Unit> ): ApiResponse<Unit>
// ========== 四、餐品净菜包装接口 ========== // ========== 四、餐品净菜包装接口 ==========
@@ -134,7 +132,7 @@ interface ApiServiceV2 {
@POST @POST
suspend fun addMealPackage( suspend fun addMealPackage(
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/meal-package/add", @Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/meal-package/add",
@Body param: NutSupMealPackageDTO @Body param: Map<String, @JvmSuppressWildcards Any>
): ApiResponse<Unit> ): ApiResponse<Unit>
} }
@@ -8,9 +8,7 @@ import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.model.NutMaterOptionVO import com.shuwei.dish.match.model.NutMaterOptionVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.NutProdComboVideoVO import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutSupCleanPackageDTO
import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutSupMealPackageDTO
import com.shuwei.dish.match.model.NutSupPkgDeviceVO import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -81,6 +79,49 @@ class NetViewModelV2(
} }
} }
/**
* 字典项下拉(回调版本),不依赖 StateFlow,适合多次不同 dictType 的场景
*/
fun getDictItemsWithCallback(
dictType: String,
onLoading: () -> Unit = {},
onResult: (UiState<List<NutDictItemVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getDictItems(dictType))
}
}
/**
* 餐品下拉(回调版本),不依赖 StateFlow,适合多次调用的场景
*/
fun getFoodOptionsWithCallback(
keyword: String? = null,
pageNum: Int = 1,
pageSize: Int = 10,
onLoading: () -> Unit = {},
onResult: (UiState<Page<NutFoodOptionVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getFoodOptions(keyword, pageNum, pageSize))
}
}
/**
* 设备下拉(回调版本),不依赖 StateFlow,适合多次调用的场景
*/
fun getDeviceOptionsWithCallback(
onLoading: () -> Unit = {},
onResult: (UiState<List<NutSupPkgDeviceVO>?>) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getDeviceOptions())
}
}
// 按溯源码查食材信息 // 按溯源码查食材信息
private val _ingredientByTraceState = MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle) private val _ingredientByTraceState = MutableStateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>>(UiState.Idle)
val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> = _ingredientByTraceState.asStateFlow() val ingredientByTraceState: StateFlow<UiState<List<NutSupHygieneIngredientOptionVO>?>> = _ingredientByTraceState.asStateFlow()
@@ -143,7 +184,7 @@ class NetViewModelV2(
private val _addCleanPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle) private val _addCleanPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addCleanPackageState: StateFlow<UiState<Unit?>> = _addCleanPackageState.asStateFlow() val addCleanPackageState: StateFlow<UiState<Unit?>> = _addCleanPackageState.asStateFlow()
fun addCleanPackage(param: NutSupCleanPackageDTO) { fun addCleanPackage(param: Map<String, Any>) {
viewModelScope.launch { viewModelScope.launch {
_addCleanPackageState.value = UiState.Loading _addCleanPackageState.value = UiState.Loading
_addCleanPackageState.value = repository.addCleanPackage(param) _addCleanPackageState.value = repository.addCleanPackage(param)
@@ -155,7 +196,7 @@ class NetViewModelV2(
private val _addMealPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle) private val _addMealPackageState = MutableStateFlow<UiState<Unit?>>(UiState.Idle)
val addMealPackageState: StateFlow<UiState<Unit?>> = _addMealPackageState.asStateFlow() val addMealPackageState: StateFlow<UiState<Unit?>> = _addMealPackageState.asStateFlow()
fun addMealPackage(param: NutSupMealPackageDTO) { fun addMealPackage(param: Map<String, Any>) {
viewModelScope.launch { viewModelScope.launch {
_addMealPackageState.value = UiState.Loading _addMealPackageState.value = UiState.Loading
_addMealPackageState.value = repository.addMealPackage(param) _addMealPackageState.value = repository.addMealPackage(param)
@@ -7,9 +7,7 @@ import com.shuwei.dish.match.model.NutFoodOptionVO
import com.shuwei.dish.match.model.NutMaterOptionVO import com.shuwei.dish.match.model.NutMaterOptionVO
import com.shuwei.dish.match.model.NutProdComboTaskDTO import com.shuwei.dish.match.model.NutProdComboTaskDTO
import com.shuwei.dish.match.model.NutProdComboVideoVO import com.shuwei.dish.match.model.NutProdComboVideoVO
import com.shuwei.dish.match.model.NutSupCleanPackageDTO
import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO import com.shuwei.dish.match.model.NutSupHygieneIngredientOptionVO
import com.shuwei.dish.match.model.NutSupMealPackageDTO
import com.shuwei.dish.match.model.NutSupPkgDeviceVO import com.shuwei.dish.match.model.NutSupPkgDeviceVO
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -81,12 +79,12 @@ class RemoteRepositoryV2 {
// ========== 单品净菜包装接口 ========== // ========== 单品净菜包装接口 ==========
suspend fun addCleanPackage(param: NutSupCleanPackageDTO): UiState<Unit?> = suspend fun addCleanPackage(param: Map<String, Any>): UiState<Unit?> =
safeApiCall { apiService.addCleanPackage(param = param) } safeApiCall { apiService.addCleanPackage(param = param) }
// ========== 餐品净菜包装接口 ========== // ========== 餐品净菜包装接口 ==========
suspend fun addMealPackage(param: NutSupMealPackageDTO): UiState<Unit?> = suspend fun addMealPackage(param: Map<String, Any>): UiState<Unit?> =
safeApiCall { apiService.addMealPackage(param = param) } safeApiCall { apiService.addMealPackage(param = param) }
} }
@@ -1,10 +1,8 @@
package com.shuwei.dish.match.ui package com.shuwei.dish.match.ui
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FormFieldAdapter import com.shuwei.dish.match.adapter.FormFieldAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
@@ -12,9 +10,14 @@ import com.shuwei.dish.match.databinding.ActivityCleanPackBinding
import com.shuwei.dish.match.model.DictType import com.shuwei.dish.match.model.DictType
import com.shuwei.dish.match.model.FieldType import com.shuwei.dish.match.model.FieldType
import com.shuwei.dish.match.model.FormField import com.shuwei.dish.match.model.FormField
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.net.NetViewModelV2
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.ext.gone 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 com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** /**
* 净菜包装表单页面,根据类型显示不同的表单字段 * 净菜包装表单页面,根据类型显示不同的表单字段
@@ -34,6 +37,17 @@ class CleanPackActivity : BaseActivity() {
private lateinit var binding: ActivityCleanPackBinding private lateinit var binding: ActivityCleanPackBinding
private val fields = mutableListOf<FormField>() private val fields = mutableListOf<FormField>()
private val formAdapter by lazy { FormFieldAdapter(this, fields) } private val formAdapter by lazy { FormFieldAdapter(this, fields) }
private val viewModelV2 by lazy { NetViewModelV2() }
private var packType: Int = TYPE_CLEAN
// 字段引用,用于跨方法访问
private var traceCodeField: FormField? = null
private var cleanNameField: FormField? = null
private var packageMethodField: FormField? = null
private var deviceField: FormField? = null
private var foodOptionsField: FormField? = null
private var packagingSpecField: FormField? = null
private var traceCodeJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -42,7 +56,7 @@ class CleanPackActivity : BaseActivity() {
binding = ActivityCleanPackBinding.inflate(layoutInflater) binding = ActivityCleanPackBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setHeaderBackground() setHeaderBackground()
val packType = intent.getIntExtra(EXTRA_TYPE, TYPE_CLEAN) packType = intent.getIntExtra(EXTRA_TYPE, TYPE_CLEAN)
// 根据类型设置标题 // 根据类型设置标题
val title = when (packType) { val title = when (packType) {
@@ -84,7 +98,13 @@ class CleanPackActivity : BaseActivity() {
} }
binding.btnSubmit.setOnClickListener { binding.btnSubmit.setOnClickListener {
// TODO: 提交表单数据 hideKeyboard()
if (!validateFields()) return@setOnClickListener
when (packType) {
TYPE_CLEAN -> submitCleanPackage()
TYPE_MEAL -> submitMealPackage()
}
} }
} }
@@ -94,33 +114,38 @@ class CleanPackActivity : BaseActivity() {
*/ */
private fun initCleanPackFields() { private fun initCleanPackFields() {
fields.clear() fields.clear()
fields.add(
FormField( // 溯源码
traceCodeField = FormField(
label = "溯源码", label = "溯源码",
type = FieldType.TEXT, type = FieldType.TEXT,
apiKey = "traceCode", apiKey = "traceCode",
required = true required = true
) )
) fields.add(traceCodeField!!)
fields.add(
FormField( // 净菜名称
cleanNameField = FormField(
label = "净菜名称", label = "净菜名称",
type = FieldType.TEXT, type = FieldType.TEXT,
apiKey = "cleanName", apiKey = "cleanName",
required = true required = true
) )
) fields.add(cleanNameField!!)
fields.add(
FormField( // 包装方式(下拉)
packageMethodField = FormField(
label = "包装方式", label = "包装方式",
type = FieldType.TEXT, type = FieldType.DROPDOWN,
apiKey = "packageMethod", extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
required = true required = true
) )
) fields.add(packageMethodField!!)
fields.add( fields.add(
FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = true) FormField(label = "包装规格", type = FieldType.TEXT, apiKey = "spec", required = true)
) )
fields.add( fields.add(
FormField( FormField(
label = "包装份数", label = "包装份数",
@@ -130,6 +155,7 @@ class CleanPackActivity : BaseActivity() {
hint = "请输入整数" hint = "请输入整数"
) )
) )
fields.add( fields.add(
FormField( FormField(
label = "包装日期", label = "包装日期",
@@ -138,6 +164,7 @@ class CleanPackActivity : BaseActivity() {
required = true required = true
) )
) )
fields.add( fields.add(
FormField( FormField(
label = "保质期至", label = "保质期至",
@@ -146,6 +173,7 @@ class CleanPackActivity : BaseActivity() {
required = true required = true
) )
) )
fields.add( fields.add(
FormField( FormField(
label = "存放温度(℃)", label = "存放温度(℃)",
@@ -155,6 +183,7 @@ class CleanPackActivity : BaseActivity() {
hint = "请输入小数" hint = "请输入小数"
) )
) )
fields.add( fields.add(
FormField( FormField(
label = "包装人", label = "包装人",
@@ -163,10 +192,23 @@ class CleanPackActivity : BaseActivity() {
required = false required = false
) )
) )
fields.add(
FormField(label = "终端设备", type = FieldType.TEXT, apiKey = "", required = false) // 终端设备(下拉)
deviceField = FormField(
label = "终端设备",
type = FieldType.DROPDOWN,
extraApiKeys = mapOf("deviceId" to "id"),
required = false
) )
fields.add(deviceField!!)
formAdapter.submitList(fields.filter { !it.hidden }) formAdapter.submitList(fields.filter { !it.hidden })
// 监听溯源码输入,自动填充净菜名称
setupTraceCodeQuery()
// 加载下拉选项
loadDictItems("sup_package_method", packageMethodField!!)
loadDeviceOptions()
} }
/** /**
@@ -176,37 +218,29 @@ class CleanPackActivity : BaseActivity() {
private fun initMealPackFields() { private fun initMealPackFields() {
fields.clear() fields.clear()
// 包装餐品 // 包装餐品(下拉)
fields.add( foodOptionsField = FormField(
FormField(
label = "包装餐品", type = FieldType.DROPDOWN, label = "包装餐品", type = FieldType.DROPDOWN,
apiKey = "foodId", required = true, extraApiKeys = mapOf("foodId" to "id"),
options = listOf( required = true
DictType(id = "1", value = "1", type = "宫保鸡丁"),
DictType(id = "2", value = "2", type = "鱼香肉丝"),
DictType(id = "3", value = "3", type = "红烧茄子"),
)
)
) )
fields.add(foodOptionsField!!)
// 包装规格 // 包装规格(下拉)
fields.add( packagingSpecField = FormField(
FormField(
label = "包装规格", type = FieldType.DROPDOWN, label = "包装规格", type = FieldType.DROPDOWN,
apiKey = "spec", required = true, extraApiKeys = mapOf("spec" to "value"),
options = listOf( required = true
DictType(id = "1", value = "500g/盒", type = "500g/盒"),
DictType(id = "2", value = "1kg/盒", type = "1kg/盒"),
)
)
) )
fields.add(packagingSpecField!!)
// 包装方式 // 包装方式(下拉)
fields.add( fields.add(
FormField( FormField(
label = "包装方式", type = FieldType.DROPDOWN, label = "包装方式", type = FieldType.DROPDOWN,
apiKey = "packageMethod", required = true, extraApiKeys = mapOf("packageMethodId" to "id", "packageMethod" to "value"),
options = listOf( required = true,
options = mutableListOf(
DictType(id = "1", value = "真空包装", type = "真空包装"), DictType(id = "1", value = "真空包装", type = "真空包装"),
DictType(id = "2", value = "气调包装", type = "气调包装"), DictType(id = "2", value = "气调包装", type = "气调包装"),
) )
@@ -223,24 +257,28 @@ class CleanPackActivity : BaseActivity() {
// 包含食材(子列表容器) // 包含食材(子列表容器)
val ingredientTemplate = listOf( val ingredientTemplate = listOf(
FormField(label = "食材溯源码", type = FieldType.TEXT, required = true, hint = "溯源码"), FormField(label = "食材溯源码", type = FieldType.TEXT, apiKey = "traceCode", required = true, hint = "溯源码"),
FormField( FormField(
label = "食材名称", type = FieldType.DROPDOWN, required = true, label = "食材名称", type = FieldType.DROPDOWN,
options = listOf( extraApiKeys = mapOf("materId" to "id", "ingredientName" to "value"),
required = true,
options = mutableListOf(
DictType(id = "1", value = "土豆", type = "土豆"), DictType(id = "1", value = "土豆", type = "土豆"),
DictType(id = "2", value = "鸡肉", type = "鸡肉"), DictType(id = "2", value = "鸡肉", type = "鸡肉"),
DictType(id = "3", value = "青椒", type = "青椒"), DictType(id = "3", value = "青椒", type = "青椒"),
) )
), ),
FormField( FormField(
label = "分类", type = FieldType.DROPDOWN, required = true, label = "分类", type = FieldType.DROPDOWN,
options = listOf( extraApiKeys = mapOf("classify" to "value"),
required = true,
options = mutableListOf(
DictType(id = "1", value = "蔬菜类", type = "蔬菜类"), DictType(id = "1", value = "蔬菜类", type = "蔬菜类"),
DictType(id = "2", value = "肉禽类", type = "肉禽类"), DictType(id = "2", value = "肉禽类", type = "肉禽类"),
DictType(id = "3", value = "水产类", type = "水产类"), DictType(id = "3", value = "水产类", type = "水产类"),
) )
), ),
FormField(label = "每份用量", type = FieldType.TEXT, required = true, hint = "如 120g"), FormField(label = "每份用量", type = FieldType.TEXT, apiKey = "amount", required = true, hint = "如 120g"),
) )
fields.add( fields.add(
FormField( FormField(
@@ -283,24 +321,21 @@ class CleanPackActivity : BaseActivity() {
) )
) )
// 终端设备 // 终端设备(下拉)
fields.add( deviceField = FormField(
FormField(
label = "终端设备", type = FieldType.DROPDOWN, label = "终端设备", type = FieldType.DROPDOWN,
apiKey = "deviceId", required = false, extraApiKeys = mapOf("deviceId" to "id"),
options = listOf( required = false
DictType(id = "1", value = "1", type = "包装机-01"),
DictType(id = "2", value = "2", type = "包装机-02"),
)
)
) )
fields.add(deviceField!!)
// 操作类型 // 操作类型(下拉)
fields.add( fields.add(
FormField( FormField(
label = "操作类型", type = FieldType.DROPDOWN, label = "操作类型", type = FieldType.DROPDOWN,
apiKey = "opType", required = true, extraApiKeys = mapOf("opType" to "value"),
options = listOf( required = true,
options = mutableListOf(
DictType(id = "1", value = "1", type = "设备自动"), DictType(id = "1", value = "1", type = "设备自动"),
DictType(id = "2", value = "2", type = "人工录入"), DictType(id = "2", value = "2", type = "人工录入"),
) )
@@ -308,6 +343,287 @@ class CleanPackActivity : BaseActivity() {
) )
formAdapter.submitList(fields.filter { !it.hidden }) formAdapter.submitList(fields.filter { !it.hidden })
// 加载下拉选项
loadFoodOptionsWithKeyword()
loadDictItemsWithCallback("sup_package_spec", packagingSpecField!!)
loadDeviceOptionsWithCallbackForMeal()
}
/**
* 监听溯源码字段变化,输入后 800ms 无变化则触发查询
*/
private fun setupTraceCodeQuery() {
val field = traceCodeField ?: return
val cleanField = cleanNameField ?: return
traceCodeJob = lifecycleScope.launch {
var lastValue = ""
while (true) {
val current = field.value.trim()
if (current != lastValue && current.isNotBlank()) {
lastValue = current
viewModelV2.getIngredientByTrace(current)
}
delay(800)
}
}
lifecycleScope.launch {
viewModelV2.ingredientByTraceState.collect { state ->
when (state) {
is UiState.Success -> {
val data = state.data
if (!data.isNullOrEmpty()) {
val info = data[0]
cleanField.value = info.materName ?: ""
// 通知 adapter 刷新净菜名称字段(索引为 1)
val idx = fields.indexOf(cleanField)
if (idx >= 0) formAdapter.notifyItemChanged(idx)
}
}
is UiState.Error -> {
toast(state.msg)
}
else -> {}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
traceCodeJob?.cancel()
}
/**
* 加载餐品下拉选项(回调版本)
*/
private fun loadFoodOptionsWithKeyword() {
val field = foodOptionsField ?: return
viewModelV2.getFoodOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.records?.map { vo ->
DictType(
id = vo.foodId?.toString() ?: "",
value = vo.foodId?.toString() ?: "",
type = vo.foodName ?: ""
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载餐品列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载字典项下拉(回调版本),适合多次不同 dictType 的场景
*/
private fun loadDictItemsWithCallback(dictType: String, targetField: FormField) {
viewModelV2.getDictItemsWithCallback(
dictType = dictType,
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.dictCode ?: "",
type = vo.dictLabel ?: ""
)
} ?: emptyList()
targetField.options.clear()
targetField.options.addAll(options)
val index = fields.indexOf(targetField)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载字典项失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载设备下拉(回调版本),用于餐品净菜包装
*/
private fun loadDeviceOptionsWithCallbackForMeal() {
val field = deviceField ?: return
viewModelV2.getDeviceOptionsWithCallback(
onLoading = {},
onResult = { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.id?.toString() ?: "",
type = "${vo.deviceName ?: ""}(${vo.deviceNo ?: ""})"
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载设备列表失败:${state.msg}")
}
else -> {}
}
}
)
}
/**
* 加载字典项下拉选项
* @param dictType 字典类型编码
* @param targetField 目标字段
*/
private fun loadDictItems(dictType: String, targetField: FormField) {
viewModelV2.getDictItems(dictType)
lifecycleScope.launch {
viewModelV2.dictItemsState.collect { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.dictCode ?: "",
type = vo.dictLabel ?: ""
)
} ?: emptyList()
targetField.options.clear()
targetField.options.addAll(options)
val index = fields.indexOf(targetField)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载字典项失败:${state.msg}")
}
else -> {}
}
}
}
}
/**
* 加载设备下拉选项
*/
private fun loadDeviceOptions() {
viewModelV2.getDeviceOptions()
val field = deviceField ?: return
lifecycleScope.launch {
viewModelV2.deviceOptionsState.collect { state ->
when (state) {
is UiState.Success -> {
val options = state.data?.map { vo ->
DictType(
id = vo.id?.toString() ?: "",
value = vo.id?.toString() ?: "",
type = "${vo.deviceName ?: ""}(${vo.deviceNo ?: ""})"
)
} ?: emptyList()
field.options.clear()
field.options.addAll(options)
val index = fields.indexOf(field)
if (index >= 0) formAdapter.notifyItemChanged(index)
}
is UiState.Error -> {
toast("加载设备列表失败:${state.msg}")
}
else -> {}
}
}
}
}
/**
* 校验必填字段
*/
private fun validateFields(): Boolean {
for (field in fields) {
if (field.hidden) continue
if (field.required) {
if (field.type == FieldType.CHILDREN) {
if (field.childrenRows.isEmpty()) {
toast("请至少添加一条${field.label}")
return false
}
// 校验子行必填
for ((rowIdx, row) in field.childrenRows.withIndex()) {
for (child in row) {
if (child.required && child.value.isBlank()) {
toast("${rowIdx + 1}${child.label}不能为空")
return false
}
}
}
} else if (field.value.isBlank()) {
toast("${field.label}不能为空")
return false
}
}
}
return true
}
/**
* 收集字段提交值,根据 apiKey / extraApiKeys 规则汇总
* @return 提交参数 Mapkey 为接口参数名,value 为提交值
*/
private fun collectSubmitValues(): MutableMap<String, String> {
val params = mutableMapOf<String, String>()
for (field in fields) {
if (field.hidden) continue
// 跳过 CHILDREN 类型,暂不处理
if (field.type == FieldType.CHILDREN) continue
if (field.extraApiKeys.isNotEmpty()) {
// 下拉字段:直接读取 bindDropdown 已解析好的 extraValues
field.extraValues.forEach { (apiKey, value) ->
params[apiKey] = value
}
} else if (field.apiKey.isNotBlank()) {
// 普通字段
params[field.apiKey] = field.value
}
}
return params
}
/**
* 提交单品净菜包装
*/
private fun submitCleanPackage() {
val params = collectSubmitValues()
viewModelV2.addCleanPackage(params)
toast("提交成功")
finish()
}
/**
* 提交餐品净菜包装
*/
private fun submitMealPackage() {
val params = collectSubmitValues()
viewModelV2.addMealPackage(params)
toast("提交成功")
finish()
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB